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 text | What to check | Policy fix |
|---|---|---|
| No Access-Control-Allow-Origin header | Origin request header | Add the exact origin to AllowedOrigins |
| Response to preflight request does not pass | OPTIONS request method | Add the intended method to AllowedMethods |
| Request header field Content-Type is not allowed | Access-Control-Request-Headers | Add Content-Type to AllowedHeaders |
| Request header field x-amz-acl is not allowed | Upload ACL or metadata headers | Add x-amz-acl or x-amz-* to AllowedHeaders |
| ETag is missing in JavaScript | Response headers app reads | Add ETag to ExposeHeaders |
Checklist for fixing R2 CORS
- Copy the frontend origin exactly:
https://app.example.comis different fromhttp://localhost:3000. - Check
Access-Control-Request-Method. Uploads usually needPUTorPOST. - Check
Access-Control-Request-Headers. Add every listed header toAllowedHeaders. - If your app reads
ETag, add it toExposeHeaders. - If cookies or credentials are involved, avoid
AllowedOrigins: ["*"]. - 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 value | Policy field | Example fix |
|---|---|---|
| Origin: http://localhost:5173 | AllowedOrigins | Add the exact local dev origin |
| Access-Control-Request-Method: PUT | AllowedMethods | Add PUT for presigned uploads |
| Access-Control-Request-Headers: content-type,x-amz-meta-user-id | AllowedHeaders | Add Content-Type and x-amz-* |
| response.headers.get('ETag') is null | ExposeHeaders | Expose 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.
Related Tools
Related Articles
Cloudflare R2 CORS Generator: Create Bucket CORS JSON for Browser Apps
Generate Cloudflare R2 CORS JSON for public reads, browser uploads, signed downloads, and presigned URL workflows. Learn AllowedOrigins, AllowedMethods, AllowedHeaders, ExposeHeaders, MaxAgeSeconds, and Wrangler-ready config.
R2 Presigned URL CORS: Fix Browser Upload and Download Errors
Cloudflare R2 presigned URLs still need bucket CORS when used from a browser. Learn how to allow PUT, POST, GET, Content-Type, x-amz-* headers, ETag exposure, localhost origins, and preflight requests.
S3 CORS Policy Generator: Create AWS Bucket CORS JSON for Browser Uploads
Generate AWS S3 CORS policies for browser uploads, public reads, signed downloads, and presigned URLs. Learn CORSRules, AllowedOrigins, AllowedMethods, AllowedHeaders, ExposeHeaders, MaxAgeSeconds, and put-bucket-cors.