Skip to content

fix(media): Range requests to /media return 500 when storage is Cloudflare R2 #3786

Description

@jdconley

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-1023500 {"error":"internal error"}
  • Range: bytes=0-99416

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

  1. Configure the relay with BUZZ_S3_ENDPOINT pointing at a Cloudflare R2 bucket (https://<account>.r2.cloudflarestorage.com).
  2. Upload a video through Buzz Desktop.
  3. curl -r 0-1023 https://<relay>/media/<sha256>500.
  4. 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_streambucket.get_object_stream(key) — works
  • get_rangebucket.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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions