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.
A valid R2 presigned URL can still fail in the browser
Presigned URLs handle permission, but CORS handles browser access. If your React, Next.js, Vue, or plain JavaScript app uploads directly to Cloudflare R2, the bucket still needs a CORS policy that matches the request origin, method, and headers. Use Spoold's S3/R2 CORS Generator & Debugger to test the exact request shape.
Why presigned URLs need CORS
A presigned URL proves that the request is authorized. It does not bypass the browser's same-origin rules. When JavaScript sends a PUT, POST, or GET to an R2 custom domain or public bucket URL, the browser checks whether R2 allows that frontend origin. If the CORS policy is missing or incomplete, the browser blocks the response.
Common R2 presigned URL CORS failures
| Browser symptom | Likely cause | Fix |
|---|---|---|
| No Access-Control-Allow-Origin header | Origin not listed | Add the exact app origin |
| Preflight request does not pass | PUT or POST missing | Add the upload method |
| Content-Type is not allowed | Header missing | Add Content-Type to AllowedHeaders |
| x-amz-meta-* blocked | Metadata header missing | Add exact metadata header or x-amz-* |
| ETag is undefined in JS | Header not exposed | Add ETag to ExposeHeaders |
Recommended CORS policy for R2 presigned PUT uploads
Start with a narrow origin list. Include localhost only for development. Add only the methods and headers your upload code sends.
[
{
"AllowedOrigins": [
"https://app.example.com",
"http://localhost:3000"
],
"AllowedMethods": ["PUT", "POST", "HEAD"],
"AllowedHeaders": [
"Content-Type",
"x-amz-acl",
"x-amz-meta-*"
],
"ExposeHeaders": ["ETag"],
"MaxAgeSeconds": 3600
}
]What to inspect in DevTools
- Open the Network tab and find the failed
OPTIONSrequest. - Copy the
Originrequest header exactly. - Copy
Access-Control-Request-Method. It should match an allowed method. - Copy
Access-Control-Request-Headers. Every listed header must be allowed. - Paste those values into the R2 CORS debugger and compare the generated fixed config.
Headers that often surprise upload code
Upload libraries and fetch wrappers can add headers you did not type manually. Watch for Content-Type, Content-MD5, x-amz-checksum-*, x-amz-acl, and custom metadata headers. If the browser sends a header during preflight, the R2 CORS policy needs to allow it.
Why curl succeeds while the browser fails
curl does not enforce browser CORS. It can upload to a valid presigned URL even when the bucket policy would block JavaScript. To simulate browser behavior with curl, send an Origin header and, for preflight checks, an OPTIONS request with Access-Control-Request-Method.
curl -i -X OPTIONS 'https://files.example.com/upload/object.jpg' \
-H 'Origin: https://app.example.com' \
-H 'Access-Control-Request-Method: PUT' \
-H 'Access-Control-Request-Headers: Content-Type,x-amz-acl'Use the debugger before changing backend code
If the presigned URL works outside the browser, start with CORS. Paste your current policy into the S3/R2 CORS debugger, enter the exact browser request details, and apply the smallest fixed policy that passes.
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.
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.
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.