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.

By Spoold Editorial TeamReviewed for tool accuracy
Editorial Policy

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.

Read the preflight request before changing code

The fastest way to fix a Cloudflare R2 CORS error is to inspect the browser's OPTIONS request. The preflight request is the browser asking R2 whether the real upload or download is allowed. It normally includes Origin, Access-Control-Request-Method, and sometimes Access-Control-Request-Headers. Those values map directly to AllowedOrigins, AllowedMethods, and AllowedHeaders in your R2 policy.

If there is no preflight request, the failing request may be a simple GET or the network error may be happening before CORS is evaluated. In that case, check DNS, the custom domain, object path, signed URL expiration, and whether the response contains any CORS headers at all.

DevTools valuePolicy fieldExample fix
Origin: http://localhost:5173AllowedOriginsAdd the exact local dev origin
Access-Control-Request-Method: PUTAllowedMethodsAdd PUT for presigned uploads
Access-Control-Request-Headers: content-type,x-amz-meta-user-idAllowedHeadersAdd Content-Type and x-amz-*
response.headers.get('ETag') is nullExposeHeadersExpose ETag

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.

What not to fix in the first pass

Avoid changing your signing code, bucket permissions, worker routes, and frontend upload component all at once. CORS errors are easier to solve when one variable moves at a time. First, prove that the object URL or presigned URL works outside the browser. Then prove that the browser preflight is allowed. Only after that should you look for deeper authorization or object-key issues.

Also avoid leaving a temporary wildcard policy in place after debugging. A broad policy may make the browser error disappear, but it can hide the actual app origin and header requirements. Replace it with an exact policy once you know the working request shape.

If a CDN, worker, or custom domain sits in front of R2, confirm that the request you are testing reaches the same path you configured. A browser console error may mention CORS even when the response came from a redirect, cached error page, or upstream route that never used the updated bucket policy.

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.

Cloudflare R2 CORS error FAQ

Does a successful curl request mean CORS is fixed?

No. curl does not enforce browser CORS. Use curl with an OPTIONS preflight shape or test from the browser to confirm the policy.

Do I need OPTIONS in AllowedMethods?

Usually you allow the real method such as PUT, POST, or GET. The browser sends OPTIONS as the preflight check for that method.

Why does production work but localhost fails?

Localhost is a different origin, and the port matters. Add the exact dev URL while testing, such as http://localhost:5173 or http://localhost:3000.

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 | Blog | Spoold