R2 range-request proxy module
A read-only /data/* route that serves R2 objects with correct HTTP range
support: Accept-Ranges, 206 responses with Content-Range, suffix ranges
(bytes=-N), HEAD returning Content-Length, and CORS with
Access-Control-Expose-Headers so a cross-origin reader can actually read
those headers back.
Almost anything that seeks into a large object needs this — <video>/<audio>
scrubbing, resumable downloads, and especially client-side analytics
(duckdb-wasm, Parquet, Arrow): the failure mode when range support is wrong
isn’t an error, it’s a correct-but-catastrophically-slow whole-object fetch on
every read, which doesn’t show up in tests that only assert correctness.
Opt-in: with DATA_BUCKET unset (the default), every request 404s, so a
project that hasn’t provisioned a bucket yet still builds, deploys, and tests
clean.
Touch-points
Section titled “Touch-points”apps/worker/src/modules/r2-proxy/range.ts— pureparseRange(header, size), no I/O. Returns{ type: 'none' | 'single' | 'unsatisfiable' }.apps/worker/src/modules/r2-proxy/routes.ts— the Hono sub-app (r2Proxy) mounted at/data.apps/worker/src/index.ts— oneapp.route('/data', r2Proxy)line.apps/worker/src/env.ts—DATA_BUCKET?: R2BucketonEnv.apps/worker/wrangler.toml— commented-out[[r2_buckets]]block.apps/worker/vitest.config.ts—r2Buckets: ['DATA_BUCKET']underminiflare, so the test environment has a working local bucket even though the real binding ships commented out.
Removal steps
Section titled “Removal steps”- Delete
apps/worker/src/modules/r2-proxy/. - In
apps/worker/src/index.ts, remove ther2Proxyimport and theapp.route('/data', r2Proxy)line. - Remove the
DATA_BUCKET?: R2Bucketfield fromapps/worker/src/env.ts. - Remove the commented
[[r2_buckets]]block fromapps/worker/wrangler.toml. - Remove
r2Buckets: ['DATA_BUCKET']fromapps/worker/vitest.config.ts. - Delete this page (
apps/docs/src/content/docs/modules/r2-proxy.md) and the links to it from the modules index and the new-project checklist — a broken internal link fails the docs build. - Run
pnpm checkto confirm the rest of the suite is still green with the module gone.
Enabling it for a real deployment
Section titled “Enabling it for a real deployment”wrangler r2 bucket create <bucket-name>.- Uncomment the
[[r2_buckets]]block inapps/worker/wrangler.tomland setbucket_name. - Deploy.
DATA_BUCKETis now bound and/data/*serves real objects.
This route is public
Section titled “This route is public”/data/* has no auth check and serves with access-control-allow-origin: *.
Any object put in DATA_BUCKET is readable by anyone who has (or guesses) its
key, from any origin — the account module’s sessions do not gate this route.
Don’t put anything non-public in DATA_BUCKET; if a project needs access
control on served objects, add it to this module before enabling it.
Same-origin content safety
Section titled “Same-origin content safety”This route serves whatever Content-Type was set when an object was written
to R2, on the app’s own origin — the same origin the auth module’s session
cookies live on. Every response forces Content-Disposition: attachment,
X-Content-Type-Options: nosniff, and Content-Security-Policy: sandbox, so
an object stored with (or overridden to) a renderable content-type like
text/html can’t execute as this origin if someone links or navigates to it
directly. attachment only affects top-level navigation — it doesn’t block
subresource loads (<video src>, <img src>, fetch()), so this doesn’t
interfere with the module’s own use cases.
Cache-control
Section titled “Cache-control”Object keys under snapshots/, events/, or raw/ — partitions this module
assumes are written once and never mutated in place — get
public, max-age=31536000, immutable. Every other key gets a short
public, max-age=300, so a re-served/overwritten object doesn’t stay stale in
intermediate caches. Adjust IMMUTABLE_PREFIXES in routes.ts to match your
own bucket layout.
Known limitation
Section titled “Known limitation”The two HEAD /data/* tests in routes.test.ts are skipped. An HTTP request
with method HEAD that reads R2 storage crashes
@cloudflare/vitest-pool-workers@0.9.14’s isolated-storage teardown —
confirmed with a minimal repro (a bare Hono app doing nothing but one
bucket.head() call, no ranges, no route mounting) that it’s the HEAD
method itself tripping the pop assertion, not this module’s code. The
identical R2 read through a GET request is fully covered by the tests above
and passes clean. This reproduces on both Windows and Linux CI (see
developing on Windows for the separate, Windows-only
R2 teardown flake this module also ran into). Fixing the HEAD issue needs a
vitest-pool-workers version that requires vitest ^4.1.0; this repo pins
vitest ^2.1.x workspace-wide, so the upgrade is out of scope here — tracked
on the PT-50 epic’s decision log. Re-enable the two skipped tests once that
upgrade happens.