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 APIrulesformatinfra/r2/apply-cors.shβ applies them viawrangler 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.
# 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-filesBuckets with browser-direct access
| Bucket | Used by | Methods |
|---|---|---|
accessible-pdf-files | Gevell PDF (pdf.gevell.com), Gevell Slides (pptx.gevell.com) | PUT (upload), GET/HEAD (download) |
accessible-uploads | v1 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.