All Articles

AWS S3 CORS Error — Every Cause and Fix (2026)

Your browser shows 'Access to fetch blocked by CORS policy' when loading from S3 or CloudFront. Here's every cause — missing CORS config, wrong AllowedOrigins, preflight failures — and the exact fix.

DevOpsBoysApr 26, 20265 min read
Share:Tweet

Your frontend makes a request to an S3 bucket or CloudFront distribution and the browser throws:

Access to fetch at 'https://mybucket.s3.amazonaws.com/data.json' 
from origin 'https://myapp.com' has been blocked by CORS policy: 
No 'Access-Control-Allow-Origin' header is present on the requested resource.

Here's every cause and the exact fix.


What CORS Actually Is

CORS (Cross-Origin Resource Sharing) is a browser security feature. When your JavaScript at https://myapp.com tries to fetch from https://mybucket.s3.amazonaws.com, the browser first asks S3: "Are you okay with requests from myapp.com?"

If S3 doesn't respond with the right headers, the browser blocks the request. S3 actually served the file — the browser blocked your JavaScript from reading it.

CORS is enforced by browsers, not servers. Direct curl requests or Postman requests don't have CORS restrictions. If something works in Postman but fails in the browser, it's a CORS issue.


Fix 1: Add CORS Configuration to S3 Bucket

Most common cause. The bucket has no CORS policy at all.

bash
# Check current CORS config
aws s3api get-bucket-cors --bucket your-bucket-name
# Error: The CORS configuration does not exist

Fix — Add CORS policy:

json
[
  {
    "AllowedHeaders": ["*"],
    "AllowedMethods": ["GET", "HEAD"],
    "AllowedOrigins": ["https://myapp.com"],
    "ExposeHeaders": ["ETag", "Content-Length"],
    "MaxAgeSeconds": 3600
  }
]
bash
# Save as cors.json and apply
aws s3api put-bucket-cors \
  --bucket your-bucket-name \
  --cors-configuration file://cors.json

For development, temporarily allow all origins (never in production):

json
[
  {
    "AllowedHeaders": ["*"],
    "AllowedMethods": ["GET", "HEAD", "PUT", "POST", "DELETE"],
    "AllowedOrigins": ["*"],
    "ExposeHeaders": []
  }
]

Fix 2: Wrong AllowedOrigins — Exact Match Required

Symptom: CORS config exists but still getting errors.

S3 CORS AllowedOrigins requires an exact match of the Origin header the browser sends. Common mistakes:

json
// BAD — trailing slash breaks matching
"AllowedOrigins": ["https://myapp.com/"]
 
// BAD — protocol mismatch
"AllowedOrigins": ["http://myapp.com"]  // but browser sends https://
 
// BAD — port missing
"AllowedOrigins": ["https://localhost"]  // browser sends https://localhost:3000
 
// GOOD
"AllowedOrigins": ["https://myapp.com", "https://localhost:3000", "http://localhost:3000"]

Check what Origin your browser is actually sending:

bash
# In browser DevTools → Network → click the failing request → Request Headers
# Look for: Origin: https://myapp.com

Make sure that exact string is in your AllowedOrigins.

You can use * wildcards only at the start or end:

json
"AllowedOrigins": ["https://*.myapp.com"]  // matches any subdomain

Fix 3: Preflight OPTIONS Request Failing

For requests with custom headers (like Authorization) or non-GET methods, the browser sends an OPTIONS preflight request first.

Browser sends OPTIONS (preflight):
  Origin: https://myapp.com
  Access-Control-Request-Method: PUT
  Access-Control-Request-Headers: Content-Type, Authorization

S3 must respond with:
  Access-Control-Allow-Origin: https://myapp.com
  Access-Control-Allow-Methods: PUT
  Access-Control-Allow-Headers: Content-Type, Authorization

Fix — Include OPTIONS in AllowedMethods and add required headers:

json
[
  {
    "AllowedHeaders": ["Content-Type", "Authorization", "x-amz-*"],
    "AllowedMethods": ["GET", "HEAD", "PUT", "POST", "DELETE"],
    "AllowedOrigins": ["https://myapp.com"],
    "ExposeHeaders": ["ETag"],
    "MaxAgeSeconds": 3600
  }
]

AllowedHeaders must include all headers your request sends. Use ["*"] to allow all.


Fix 4: CloudFront Not Forwarding CORS Headers

Symptom: S3 CORS config is correct, but getting CORS errors through CloudFront.

CloudFront caches S3 responses. By default, it does NOT forward the Origin header from the browser to S3 — so S3 never sees which origin the request came from and can't send the right CORS headers back.

Fix — Add Origin to CloudFront cache key and forward it to origin:

bash
# Create a cache policy that includes Origin header
aws cloudfront create-cache-policy \
  --cache-policy-config '{
    "Name": "CORS-S3Origin",
    "DefaultTTL": 86400,
    "MaxTTL": 31536000,
    "MinTTL": 0,
    "ParametersInCacheKeyAndForwardedToOrigin": {
      "EnableAcceptEncodingGzip": true,
      "EnableAcceptEncodingBrotli": true,
      "HeadersConfig": {
        "HeaderBehavior": "whitelist",
        "Headers": {
          "Quantity": 2,
          "Items": ["Origin", "Access-Control-Request-Headers"]
        }
      },
      "CookiesConfig": {"CookieBehavior": "none"},
      "QueryStringsConfig": {"QueryStringBehavior": "none"}
    }
  }'

Or use the AWS managed cache policy CachingOptimizedForUncompressedObjects which already handles CORS correctly, or select "CachingDisabled" for S3 origins that need CORS.

In Console: CloudFront → Distribution → Behaviors → Edit → Cache key and origin requests → Select "Cache policy" → Use "CORS-S3Origin" managed policy.


Fix 5: S3 Static Website vs S3 REST Endpoint

Symptom: CORS works on the REST endpoint but not the website endpoint (or vice versa).

  • REST endpoint (bucket.s3.region.amazonaws.com) — uses S3 bucket CORS config
  • Website endpoint (bucket.s3-website-region.amazonaws.com) — serves content differently, can have different CORS behavior

If using CloudFront + S3 static website hosting, the CORS config on the bucket applies to the REST endpoint. For website hosting, ensure CORS is configured and the right endpoint is set as the CloudFront origin.


Fix 6: Presigned URL CORS Issues

Symptom: Regular S3 requests work, but presigned URL requests fail with CORS.

When using presigned URLs for direct browser uploads (PUT to S3), the CORS config must allow PUT from your origin:

json
[
  {
    "AllowedHeaders": ["*"],
    "AllowedMethods": ["GET", "PUT", "POST"],
    "AllowedOrigins": ["https://myapp.com"],
    "ExposeHeaders": ["ETag"]
  }
]

Also, when generating the presigned URL server-side, ensure you're not adding custom headers that aren't in AllowedHeaders.


Debugging CORS

bash
# Test CORS headers directly
curl -v \
  -H "Origin: https://myapp.com" \
  -H "Access-Control-Request-Method: GET" \
  -X OPTIONS \
  https://mybucket.s3.amazonaws.com/test.json
 
# Should see in response:
# Access-Control-Allow-Origin: https://myapp.com
# Access-Control-Allow-Methods: GET

If these headers are missing, the CORS config isn't applied or the Origin doesn't match.

Invalidate CloudFront after changing CORS config:

bash
aws cloudfront create-invalidation \
  --distribution-id YOUR_DIST_ID \
  --paths "/*"

SymptomCauseFix
No CORS headers at allCORS config missingAdd CORS policy to bucket
Origin doesn't matchTrailing slash / wrong protocolExact match required in AllowedOrigins
Preflight failsOPTIONS not in AllowedMethodsAdd OPTIONS/PUT/DELETE to AllowedMethods
Works directly, fails via CloudFrontOrigin header not forwardedAdd Origin to CloudFront cache key
Presigned URL failsPUT not in AllowedMethodsAdd PUT to CORS config

S3 CORS errors are almost always an AllowedOrigins mismatch or CloudFront not forwarding the Origin header. Check those two first.

Newsletter

Stay ahead of the curve

Get the latest DevOps, Kubernetes, AWS, and AI/ML guides delivered straight to your inbox. No spam — just practical engineering content.

Related Articles

Comments