Describe the bug
GET /media/{sha256} with a Range header fails when media storage is Cloudflare R2. The relay advertises accept-ranges: bytes on the full response, so clients trust it and then get an error:
Range: bytes=0-1023 → 500 {"error":"internal error"}
Range: bytes=0-99 → 416
A request with no Range header returns 200 with the complete body, so the object and the key resolve correctly.
This breaks video playback entirely, since players use range requests to stream and seek. Images are unaffected because they are fetched whole.
Steps to reproduce
- Configure the relay with
BUZZ_S3_ENDPOINT pointing at a Cloudflare R2 bucket (https://<account>.r2.cloudflarestorage.com).
- Upload a video through Buzz Desktop.
curl -r 0-1023 https://<relay>/media/<sha256> → 500.
curl https://<relay>/media/<sha256> (no Range) → 200, full body.
Expected behavior
206 Partial Content with Content-Range: bytes 0-1023/<total>, per the behaviour documented at crates/buzz-relay/src/api/media.rs (RFC 9110 §14.2).
Version and platform
- Buzz version: relay
ghcr.io/block/buzz:sha-63496cc
- OS: Ubuntu 24.04, Docker Compose, media on Cloudflare R2
Logs / additional context
Isolated to the relay — R2 itself handles ranges correctly:
| Check |
Result |
R2 ranged GetObject (aws-cli, same key/creds) |
ContentRange: bytes 0-1023/9896517, 1024 bytes |
R2 HeadObject |
ContentLength=9896517, AcceptRanges=bytes |
Sidecar _meta/**.json |
"size": 9896517 (correct) |
Relay full GET |
200, all 9896517 bytes |
Relay ranged GET |
500 |
Both relay paths resolve the same key via resolve_s3_key and both call head_with_metadata, so key resolution and total size are not the difference. The only divergence is in buzz-media::storage:
get_stream → bucket.get_object_stream(key) — works
get_range → bucket.get_object_range(key, start, Some(end)) — fails
A get_range failure surfaces as MediaError::StorageError, which matches the 500. This looks like a rust-s3 get_object_range incompatibility with R2 rather than a bug in the range parsing, which behaves correctly.
Relay log for the failing request:
level=ERROR message="response failed" classification="Status code: 500 Internal Server Error" latency="1206 ms" target=tower_http::trace::on_failure http.request.method=GET
Not reproduced against MinIO or AWS S3 — R2 is the only backend tested here, so this may be R2-specific S3 interop. Possibly adjacent to #2470 (S3-interop failure on GCS), though that is the git object store rather than media.
Describe the bug
GET /media/{sha256}with aRangeheader fails when media storage is Cloudflare R2. The relay advertisesaccept-ranges: byteson the full response, so clients trust it and then get an error:Range: bytes=0-1023→500{"error":"internal error"}Range: bytes=0-99→416A request with no
Rangeheader returns200with the complete body, so the object and the key resolve correctly.This breaks video playback entirely, since players use range requests to stream and seek. Images are unaffected because they are fetched whole.
Steps to reproduce
BUZZ_S3_ENDPOINTpointing at a Cloudflare R2 bucket (https://<account>.r2.cloudflarestorage.com).curl -r 0-1023 https://<relay>/media/<sha256>→500.curl https://<relay>/media/<sha256>(no Range) →200, full body.Expected behavior
206 Partial ContentwithContent-Range: bytes 0-1023/<total>, per the behaviour documented atcrates/buzz-relay/src/api/media.rs(RFC 9110 §14.2).Version and platform
ghcr.io/block/buzz:sha-63496ccLogs / additional context
Isolated to the relay — R2 itself handles ranges correctly:
GetObject(aws-cli, same key/creds)ContentRange: bytes 0-1023/9896517, 1024 bytesHeadObjectContentLength=9896517,AcceptRanges=bytes_meta/**.json"size": 9896517(correct)GET200, all 9896517 bytesGET500Both relay paths resolve the same key via
resolve_s3_keyand both callhead_with_metadata, so key resolution and total size are not the difference. The only divergence is inbuzz-media::storage:get_stream→bucket.get_object_stream(key)— worksget_range→bucket.get_object_range(key, start, Some(end))— failsA
get_rangefailure surfaces asMediaError::StorageError, which matches the500. This looks like arust-s3get_object_rangeincompatibility with R2 rather than a bug in the range parsing, which behaves correctly.Relay log for the failing request:
Not reproduced against MinIO or AWS S3 — R2 is the only backend tested here, so this may be R2-specific S3 interop. Possibly adjacent to #2470 (S3-interop failure on GCS), though that is the git object store rather than media.