Skip to content

R2 Bucket CORS

Why this exists

Several apps upload/download files directly from the browser to R2 using a presigned S3 PUT/GET URL (minted by POST /api/files/upload etc.). This bypasses the API Gateway 10 MB body limit, but it makes the request cross-origin: the browser talks to https://<account>.r2.cloudflarestorage.com, not to our API.

That means each upload bucket’s CORS policy must list every site origin that uploads to it. If an origin is missing, the browser blocks the preflight/PUT and the app’s fetch() throws a bare TypeError: Failed to fetch β€” with no HTTP status β€” which surfaces to users as ERR_UPLOAD_INCOMPLETE (β€œWe couldn’t finish uploading this file to storage.”).

The 2026 rebrand gap (root cause of the upload failures)

R2 CORS lives on Cloudflare, not in app config or wrangler.toml, so it did not move when the apps rebranded from *.theaccessible.org to *.gevell.com. Buckets kept allowing only the old theaccessible.org origins, so every user on pdf.gevell.com (and other gevell sites) had their direct-to-R2 uploads silently rejected. This was invisible because β€œFailed to fetch” carries no status and the upload path only logged to /api/client-logs.

Source of truth

CORS policies are version-controlled here:

  • infra/r2/cors/<bucket>.json β€” one file per bucket, in the R2 API rules format
  • infra/r2/apply-cors.sh β€” applies them via wrangler r2 bucket cors set

To change CORS: edit the JSON, run the script, commit. Never hand-edit CORS in the Cloudflare dashboard β€” it will drift from the repo.

Terminal window
# Apply all policies (anglin.com account c6cce84d…):
./infra/r2/apply-cors.sh
# Apply one bucket:
./infra/r2/apply-cors.sh accessible-pdf-files
# Inspect what's live:
CLOUDFLARE_ACCOUNT_ID=c6cce84d1636ec85ec946a19edef0103 \
npx wrangler r2 bucket cors list accessible-pdf-files

Buckets with browser-direct access

BucketUsed byMethods
accessible-pdf-filesGevell PDF (pdf.gevell.com), Gevell Slides (pptx.gevell.com)PUT (upload), GET/HEAD (download)
accessible-uploadsv1 uploads API (upload./admin. sites)PUT/GET/HEAD

Every gevell.com site origin is allowed on these buckets (per the product registry in packages/accessible-org-ui/src/products.ts, plus www, admin, upload, and staging). The legacy *.theaccessible.org origins are retained during the transition and http://localhost:* for local dev.

Buckets whose uploads route through the API (Node/Lambda), not the browser (accessible-forms, accessible-photos, org-chart-files, flipbook, convert-email-files), have no CORS policy and don’t need one. If one of those apps ever adds direct browser uploads, add a cors/<bucket>.json here.

Adding a new gevell site

When a new *.gevell.com site ships that uploads/downloads directly from the browser, add its origin to the relevant infra/r2/cors/<bucket>.json and run apply-cors.sh.