CORS Policy for Browser Uploads: S3, R2, Presigned URLs, and ETag
Create a CORS policy for browser uploads to object storage. Learn how origins, PUT/POST methods, Content-Type, x-amz-* headers, preflight OPTIONS, credentials, and ETag exposure work for S3 and Cloudflare R2.
Browser uploads need CORS even when authentication is already solved
Direct browser uploads to S3, Cloudflare R2, or another S3-compatible object store usually use a presigned URL. That URL authorizes the upload, but the bucket CORS policy decides whether the browser is allowed to make the cross-origin request. Spoold's S3/R2 CORS Generator & Debugger helps build and test that policy.
What belongs in a browser upload CORS policy?
A browser upload policy needs to match the actual request that JavaScript sends. For object storage uploads, that usually means a frontend origin, a write method, upload headers, exposed response headers, and a preflight cache duration.
| Policy part | Browser upload meaning | Example |
|---|---|---|
| AllowedOrigins | Where your frontend runs | https://app.example.com |
| AllowedMethods | Upload method used by the signed URL | PUT, POST, HEAD |
| AllowedHeaders | Headers fetch/XMLHttpRequest sends | Content-Type, x-amz-* |
| ExposeHeaders | Response headers JS needs after upload | ETag |
| MaxAgeSeconds | How long preflight can be cached | 3600 |
PUT vs POST browser uploads
A presigned PUT upload sends the file body directly to the object URL. A presigned POST upload usually submits form data with a policy, signature, key, and file field. Your CORS policy should include the method your backend generated.
- Use
PUTwhen frontend code callsfetch(url, { method: 'PUT' }). - Use
POSTwhen frontend code submits a form or FormData policy. - Include
HEADwhen the app checks object existence or metadata after upload.
Upload headers that trigger preflight
Browsers send an OPTIONS preflight when the request is not a simple CORS request. Uploads frequently send headers such as Content-Type, checksum headers, or S3 metadata headers. The bucket policy must allow those headers.
[
{
"AllowedOrigins": ["https://app.example.com"],
"AllowedMethods": ["PUT", "POST", "HEAD"],
"AllowedHeaders": [
"Content-Type",
"x-amz-*"
],
"ExposeHeaders": ["ETag"],
"MaxAgeSeconds": 3600
}
]Should browser uploads use wildcard origins?
A wildcard origin is tempting for quick testing, but it is usually too broad for app uploads. Use exact origins for production uploads, especially when the request includes credentials, tenant-specific URLs, or sensitive object keys. Public read assets are a better fit for broader origins.
Why ETag needs ExposeHeaders
After an upload succeeds, many apps read ETag to store the uploaded object hash or confirm the write. The browser hides most response headers from JavaScript unless the CORS policy lists them in ExposeHeaders. If upload succeeds but response.headers.get("ETag") returns null, expose the header.
How to test a browser upload policy
- Generate the presigned URL from your backend as usual.
- Open DevTools and run the upload from the browser.
- Find the OPTIONS request and copy origin, method, and request headers.
- Paste your policy and those details into the CORS debugger.
- Apply the suggested fixed config and retest the browser request.
Browser upload CORS checklist
- Origin includes protocol, host, and port.
- Methods include PUT or POST, not only GET.
- AllowedHeaders includes every preflight request header.
- ExposeHeaders includes ETag if the app reads it.
- Wildcard origins are avoided for credentialed or app-specific uploads.
- Localhost origins are included during development.
Generate and debug in one place
Use the S3/R2 CORS Generator & Debugger to create a starting policy, edit the JSON in Monaco, download provider-specific CLI JSON, and diagnose failed browser uploads from the same page.
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.
Try It Now
Put this guide into practice with our free tools. No sign-up required.
Generate Browser Upload CORS