AWS S3 error
S3 upload blocked by CORS policy: No "Access-Control-Allow-Origin" header is present on the requested resource
"Access to fetch has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource" on a PUT to a presigned S3 URL means the browser checked S3's response for your site's origin and found no CORS header. The SDK and the signature are not the cause: CORS is a rule stored on the bucket. In the S3 console open the bucket, Permissions, Cross-origin resource sharing (CORS), and add a JSON rule that lists the exact origin you upload from, including each Vercel preview origin you test on, the PUT method, and the headers the browser sends, such as Content-Type. Save it and retry the upload.
What does the browser message look like for a presigned S3 upload?
Chrome builds the text from fixed strings in its source (cors_error_string.cc): the missing-header message above and, when the check fails on the preflight request, the prefix "Response to preflight request doesn't pass access control check: " followed by a reason such as "It does not have HTTP ok status." Firefox says "Reason: CORS header 'Access-Control-Allow-Origin' missing" (MDN).
AWS documents the two S3 answers behind it. A bucket with no CORS configuration answers 403 Forbidden with "CORS is not enabled for this bucket". A bucket whose rules do not match the origin, the method or the headers answers 403 Forbidden with "This CORS request is not allowed". The checklist below sends the preflight with curl, so you can read which one you got.
Access to fetch at 'https://my-uploads.s3.eu-west-1.amazonaws.com/uploads/a.png?X-Amz-...' from origin 'https://my-app.vercel.app' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.Which CORS rule does a presigned PUT upload need?
S3 uses the first CORSRule that matches the request. For a match, the Origin header must be in AllowedOrigins, the method named in Access-Control-Request-Method must be in AllowedMethods, and each header named in Access-Control-Request-Headers must match an AllowedHeaders entry. AllowedMethods accepts GET, PUT, POST, DELETE and HEAD. The S3 console only takes JSON, written as an array of rules; the AWS CLI takes the same rules inside a CORSRules key.
AllowedHeaders with "*" is the form AWS uses in its own example. If you list headers instead, include every one the upload sends, Content-Type among them: AWS says a request header missing from the list gets a 403. MaxAgeSeconds is the time a browser may cache the preflight answer, so a retry inside that window can reuse an old answer.
[
{
"AllowedOrigins": ["https://app.example.com", "http://localhost:3000"],
"AllowedMethods": ["PUT", "GET"],
"AllowedHeaders": ["*"],
"ExposeHeaders": [],
"MaxAgeSeconds": 3000
}
]How do I allow Vercel preview deployments in the S3 CORS rule?
Vercel gives each Git commit a URL shaped <project-name>-<unique-hash>-<scope-slug>.vercel.app and each branch one shaped <project-name>-git-<branch-name>-<scope-slug>.vercel.app. The rule has to list the origin you are testing from. An S3 origin string may contain one * wildcard, so "https://my-app-*-my-team.vercel.app" matches both shapes for one project. A rule for "https://*.vercel.app" would also let every other site on vercel.app upload to your bucket, so keep the project and team parts.
Vercel shortens a generated name that would pass the 63-character limit of a DNS label. For a long project or branch name, copy the Origin header from the failing OPTIONS request instead of building the string by hand, and add your custom domain as its own entry.
"AllowedOrigins": [
"https://my-app.example.com",
"https://my-app-*-my-team.vercel.app",
"http://localhost:3000"
]Checklist
How to fix it, then check the fix
- 01In the browser Network tab, open the failing OPTIONS request to the S3 URL and copy its Origin header. That exact origin, with its scheme, must be in AllowedOrigins.
- 02Open the bucket in the S3 console, Permissions, Cross-origin resource sharing (CORS), Edit, paste the JSON rule and choose Save changes. Or use the CLI, with the same rules inside a CORSRules key in cors.json:
aws s3api put-bucket-cors --bucket my-uploads --cors-configuration file://cors.json - 03Send the preflight yourself, with your origin, as the AWS CORS testing page does. A matching rule answers 200 OK with an Access-Control-Allow-Origin header; if any requested header is not allowed, AWS says none of the CORS headers come back:
curl -v -X OPTIONS -H "Origin: https://my-app.example.com" -H "Access-Control-Request-Method: PUT" -H "Access-Control-Request-Headers: content-type" "https://my-uploads.s3.eu-west-1.amazonaws.com/uploads/a.png" - 04If a CloudFront distribution sits in front of the bucket, AWS says to allow the OPTIONS method, forward Origin, Access-Control-Request-Headers and Access-Control-Request-Method, and include the Origin header in the cache key.
- 05Retry the upload from the browser. If S3 now answers 403 with a body that names SignatureDoesNotMatch, the CORS rule works and the signature is the next problem.
Related
Related guides
Sources
Official docs behind this checklist
Browse all launch guides · Read resources · How to make and check your next change
FAQ
FAQ for builders before launch
Is this a bug in the AWS SDK or in the presigned URL?
No. The SDK only builds the URL. The browser decides about CORS from the response S3 sends, and S3 builds that response from the CORS configuration of the bucket, so the fix is a rule on the bucket.
Why does the same URL work with curl but fail in the browser?
S3 matches CORS rules against the Origin header (AWS docs). curl sends no Origin unless you add one, and a browser sends it on every cross-origin request, so a missing rule only shows up in the browser.
Should AllowedOrigins be "*"?
AWS allows "*" to enable all origins, and MDN says to use the wildcard only for public APIs. For an upload bucket, list your own origins and your preview pattern.