Back to Blog
10 min read
Web

Fix Cloudflare R2 CORS Error: Preflight, Origin, Headers, and ETag

Troubleshoot Cloudflare R2 CORS errors in browser apps. Fix missing Access-Control-Allow-Origin, failed preflight OPTIONS, blocked Content-Type or x-amz-* headers, localhost origins, and hidden ETag response headers.

R2 CORS errors are usually one missing origin, method, or header

Cloudflare R2 CORS failures look noisy in the browser, but most reduce to a small mismatch: the frontend origin is not allowed, the method is missing, a request header is blocked, or a response header is not exposed. The S3/R2 CORS debugger checks those pieces directly.

Start with the exact browser error

Do not debug R2 CORS from the backend first. Open browser DevTools, go to Network, and find the failed request. If there is an OPTIONS request before your upload or download, that is the preflight check. Copy the request headers and the console error text.

R2 CORS error quick fixes

Error textWhat to checkPolicy fix
No Access-Control-Allow-Origin headerOrigin request headerAdd the exact origin to AllowedOrigins
Response to preflight request does not passOPTIONS request methodAdd the intended method to AllowedMethods
Request header field Content-Type is not allowedAccess-Control-Request-HeadersAdd Content-Type to AllowedHeaders
Request header field x-amz-acl is not allowedUpload ACL or metadata headersAdd x-amz-acl or x-amz-* to AllowedHeaders
ETag is missing in JavaScriptResponse headers app readsAdd ETag to ExposeHeaders

Checklist for fixing R2 CORS

  1. Copy the frontend origin exactly: https://app.example.com is different from http://localhost:3000.
  2. Check Access-Control-Request-Method. Uploads usually need PUT or POST.
  3. Check Access-Control-Request-Headers. Add every listed header to AllowedHeaders.
  4. If your app reads ETag, add it to ExposeHeaders.
  5. If cookies or credentials are involved, avoid AllowedOrigins: ["*"].
  6. After changing CORS, retest in the browser and watch for cached CDN responses.

Example fixed R2 policy

This example covers a common app upload flow: local development, production app origin, PUT/POST uploads, Content-Type, x-amz metadata headers, and JavaScript access to ETag.

[
  {
    "AllowedOrigins": [
      "https://app.example.com",
      "http://localhost:3000"
    ],
    "AllowedMethods": ["PUT", "POST", "HEAD"],
    "AllowedHeaders": [
      "Content-Type",
      "x-amz-*"
    ],
    "ExposeHeaders": ["ETag"],
    "MaxAgeSeconds": 3600
  }
]

Why localhost often breaks

CORS origins include scheme, host, and port. If your policy allows https://app.example.com, it does not allow http://localhost:3000. Add each development origin explicitly, and remove it later if your production policy should be stricter.

Debug R2 CORS with a preflight curl

A normal curl request will not show the browser's CORS decision. Use an OPTIONS request with Origin, Access-Control-Request-Method, and Access-Control-Request-Headers.

curl -i -X OPTIONS 'https://files.example.com/avatar.png' \
  -H 'Origin: https://app.example.com' \
  -H 'Access-Control-Request-Method: PUT' \
  -H 'Access-Control-Request-Headers: Content-Type,x-amz-meta-user-id'

Use the R2 CORS debugger

Paste the current policy into Spoold's CORS debugger. Enter the request origin, method, request headers, response headers your app reads, and error text. The debugger returns a diagnosis and a suggested fixed config.

Try It Now

Put this guide into practice with our free tools. No sign-up required.

Fix R2 CORS Error
Fix Cloudflare R2 CORS Error: Preflight, Origin, Headers, and ETag | Spoold Blog | Spoold