Skip to content

Commit 39ce91b

Browse files
authored
fix: remove Content-Length from virtual file StreamingResponse (#8928)
Closes #8917 The `@file/` endpoint set `Content-Length` on a `StreamingResponse`, mixing length-delimited and chunked framing. Starlette's `BaseHTTPMiddleware` re-wraps streaming bodies through memory object streams, which can desynchronize the byte count from the declared length for large responses. This caused h11 `LocalProtocolError` when serving large anywidget ESM bundles (~2MB+). Fix: drop the `Content-Length` header so h11 uses chunked transfer encoding instead.
1 parent 5f487d6 commit 39ce91b

2 files changed

Lines changed: 44 additions & 1 deletion

File tree

marimo/_server/api/endpoints/assets.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -426,12 +426,15 @@ def virtual_file(
426426

427427
chunks = read_virtual_file_chunked(filename, int(byte_length))
428428
mimetype, _ = mimetypes.guess_type(filename)
429+
# Do NOT set Content-Length here. StreamingResponse with an explicit
430+
# Content-Length causes h11 LocalProtocolError ("Too little data for
431+
# declared Content-Length") for large files. Omitting it lets h11 use
432+
# chunked transfer encoding instead. See #8917.
429433
return StreamingResponse(
430434
content=chunks,
431435
media_type=mimetype,
432436
headers={
433437
"Cache-Control": "max-age=86400",
434-
"Content-Length": byte_length,
435438
},
436439
)
437440

tests/_server/api/endpoints/test_assets.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,46 @@ def test_vfile(client: TestClient) -> None:
133133
assert response.json() == {"detail": "Invalid virtual file request"}
134134

135135

136+
def test_vfile_large_streaming(client: TestClient) -> None:
137+
"""Regression test: large virtual files must stream without
138+
Content-Length mismatch (h11 LocalProtocolError).
139+
140+
See https://github.com/marimo-team/marimo/issues/8917
141+
"""
142+
from marimo._runtime.virtual_file.storage import (
143+
InMemoryStorage,
144+
VirtualFileStorageManager,
145+
)
146+
147+
manager = VirtualFileStorageManager()
148+
original_storage = manager.storage
149+
storage = InMemoryStorage()
150+
manager.storage = storage
151+
152+
try:
153+
# ~2 MB file, similar to a large anywidget ESM bundle
154+
data = b"x" * (2 * 1024 * 1024)
155+
filename = "test-large.js"
156+
storage.store(filename, data)
157+
byte_length = len(data)
158+
159+
response = client.get(
160+
f"/@file/{byte_length}-{filename}",
161+
headers=token_header(),
162+
)
163+
assert response.status_code == 200
164+
assert response.content == data
165+
assert (
166+
response.headers.get("content-type") == "text/javascript"
167+
or response.headers.get("content-type") == "application/javascript"
168+
)
169+
# StreamingResponse must NOT set Content-Length to avoid h11
170+
# LocalProtocolError with large files
171+
assert "content-length" not in response.headers
172+
finally:
173+
manager.storage = original_storage
174+
175+
136176
def test_public_file_serving(client: TestClient) -> None:
137177
# Setup app state with a mock notebook
138178
app_state = AppState.from_app(cast(Any, client.app))

0 commit comments

Comments
 (0)