fix: support HTTP Range requests on virtual files (#9460)#9473
Conversation
Safari's <audio>/<video> elements require a 206 Partial Content response to their initial range probe. Without it, mo.audio renders a disabled player. Chrome and Firefox are more forgiving, so the bug was Safari-only. Parse Range headers in the /@file/ endpoint and return 206 with Content-Range/Content-Length, advertise Accept-Ranges: bytes on every response, and 416 for invalid ranges. Full responses still omit Content-Length to preserve the h11 fix from #8928. Adds an optional start offset to the chunked virtual-file readers so partial reads don't allocate the full buffer.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Numpy → mo.audio routes through the virtual file endpoint, which is the code path the Range support fix targets. Open in Safari to verify the player is enabled.
There was a problem hiding this comment.
No issues found across 4 files
Architecture diagram
sequenceDiagram
participant SA as Safari Browser
participant AS as Any Browser (Chrome/Firefox)
participant FE as FastAPI Endpoint
participant PR as _parse_range_header()
participant VFC as read_virtual_file_chunked()
participant VSM as VirtualFileStorageManager
participant ST as Storage Backend
Note over SA,ST: HTTP Range Request Flow for Virtual Files
SA->>FE: GET /@file/{size}-{filename} (Range: bytes=0-99)
FE->>FE: Parse filename_and_length
FE->>FE: total_size = int(byte_length_str)
FE->>FE: Guess mimetype from filename
FE->>FE: Build headers with Accept-Ranges: bytes
FE->>PR: _parse_range_header("bytes=0-99", total_size)
alt Valid range
PR-->>FE: (start=0, end=99)
FE->>VFC: read_virtual_file_chunked(filename, 100, start=0)
VFC->>VSM: read_chunked(filename, 100, start=0)
VSM->>ST: read_chunked(filename, 100, 8192, start=0)
ST-->>VSM: Iterator[bytes]
VSM-->>VFC: Iterator[bytes]
VFC-->>FE: Iterator[bytes]
FE->>FE: Build Content-Range: bytes 0-99/{total_size}
FE->>FE: Build Content-Length: 100
FE-->>SA: 206 Partial Content + chunked bytes
else Invalid/unsatisfiable range
PR-->>FE: None
FE-->>SA: 416 Range Not Satisfiable + Content-Range: bytes */{total_size}
end
AS->>FE: GET /@file/{size}-{filename} (no Range header)
FE->>FE: Parse filename_and_length
FE->>FE: total_size = int(byte_length_str)
FE->>FE: Guess mimetype
FE->>FE: Build headers with Accept-Ranges: bytes
FE->>VFC: read_virtual_file_chunked(filename, total_size)
VFC->>VSM: read_chunked(filename, total_size)
VSM->>ST: read_chunked(filename, total_size, 8192)
ST-->>VSM: Iterator[bytes]
VSM-->>VFC: Iterator[bytes]
VFC-->>FE: Iterator[bytes]
Note over FE: Content-Length NOT set (chunked transfer encoding)
FE-->>AS: 200 OK + Accept-Ranges: bytes
alt Download request (?download=1)
FE->>FE: Add Content-Disposition: attachment
end
SA->>FE: GET /@file/{size}-{filename} (Range: bytes=-50)
FE->>PR: _parse_range_header("bytes=-50", total_size)
PR->>PR: suffix=50, start=total_size-50, end=total_size-1
PR-->>FE: (start, end)
FE->>VFC: read_virtual_file_chunked(filename, 50, start=start)
VFC->>VSM: read_chunked(filename, 50, start=start)
VSM->>ST: read_chunked(filename, 50, 8192, start=start)
ST-->>VSM: Iterator[bytes]
VSM-->>VFC: Iterator[bytes]
VFC-->>FE: Iterator[bytes]
FE-->>SA: 206 Partial Content + last 50 bytes
SA->>FE: GET /@file/{size}-{filename} (Range: bytes={size}-)
FE->>PR: _parse_range_header(f"bytes={size}-", total_size)
PR->>PR: start=size, end=total_size-1
PR->>PR: start >= total_size
PR-->>FE: None
FE-->>SA: 416 Range Not Satisfiable + Content-Range: bytes */{total_size}
Tip: cubic could auto-approve low-risk PRs like this, if it thinks it's safe to merge. Learn more
There was a problem hiding this comment.
Pull request overview
This PR updates the @file virtual-file endpoint to support HTTP Range requests (returning 206 Partial Content) to improve Safari <audio>/<video> playback compatibility, and extends the virtual-file storage readers to support streaming from a byte offset to avoid unnecessary allocations.
Changes:
- Add
Rangeheader parsing toGET /@file/{filename_and_length}and return206withContent-Range/Content-Length,416for unsatisfiable ranges, andAccept-Ranges: byteson responses. - Extend virtual-file chunked readers (
read_virtual_file_chunkedand storage backends) with astartoffset for partial reads. - Add endpoint-level tests covering bounded, open-ended, suffix, and out-of-range range requests.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
tests/_server/api/endpoints/test_assets.py |
Adds regression tests for Range requests against the virtual-file endpoint. |
marimo/_server/api/endpoints/assets.py |
Implements Range handling for /@file/ responses and introduces a single-range parser. |
marimo/_runtime/virtual_file/virtual_file.py |
Adds a start offset option to the chunked virtual-file reader wrapper. |
marimo/_runtime/virtual_file/storage.py |
Adds start offset support to storage backends’ read_chunked() implementations. |
Per RFC 9110 the range unit token is case-insensitive, so accept 'Bytes='/'BYTES=' too. Adds storage-level tests for the new ``start`` offset on the chunked readers.
|
🚀 Development release published. You may be able to view the changes at https://marimo.app?v=0.23.6-dev30 |
Safari's
<audio>/<video>elements require a 206 Partial Content responseto their initial range probe. Without it, mo.audio renders a disabled
player. Chrome and Firefox are more forgiving, so the bug was Safari-only.
Parse Range headers in the
/@file/endpoint and return 206 withContent-Range/Content-Length, advertiseAccept-Ranges:bytes on everyresponse, and 416 for invalid ranges. Full responses still omit
Content-Lengthto preserve the h11 fix from #8928. Adds an optional startoffset to the chunked virtual-file readers so partial reads don't
allocate the full buffer.
Closes #9460