Back to Blog
11 min read
Web

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 partBrowser upload meaningExample
AllowedOriginsWhere your frontend runshttps://app.example.com
AllowedMethodsUpload method used by the signed URLPUT, POST, HEAD
AllowedHeadersHeaders fetch/XMLHttpRequest sendsContent-Type, x-amz-*
ExposeHeadersResponse headers JS needs after uploadETag
MaxAgeSecondsHow long preflight can be cached3600

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 PUT when frontend code calls fetch(url, { method: 'PUT' }).
  • Use POST when frontend code submits a form or FormData policy.
  • Include HEAD when 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

  1. Generate the presigned URL from your backend as usual.
  2. Open DevTools and run the upload from the browser.
  3. Find the OPTIONS request and copy origin, method, and request headers.
  4. Paste your policy and those details into the CORS debugger.
  5. 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.

Try It Now

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

Generate Browser Upload CORS
CORS Policy for Browser Uploads: S3, R2, Presigned URLs, and ETag | Spoold Blog | Spoold