block / block/buzz

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

Open
#3,786 5 comments 0 reactions 0 assignees View on GitHub
Dominant language
Rust
Stars
32.7k
Forks
4.3k
Avg merge
1d 13h
Merged PRs (30d)
253

Description

**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**

1. Configure the relay with `BUZZ_S3_ENDPOINT` pointing at a Cloudflare R2 bucket (`https://.r2.cloudflarestorage.com`).
2. Upload a video through Buzz Desktop.
3. `curl -r 0-1023 https:///media/` → `500`.
4. `curl https:///media/` (no Range) → `200`, full body.

**Expected behavior**

`206 Partial Content` with `Content-Range: bytes 0-1023/`, 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.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.