Fix: AWS S3 CORS Error — Access to Fetch Blocked by CORS Policy
Quick Answer
How to fix AWS S3 CORS errors — S3 bucket CORS configuration, pre-signed URL CORS, CloudFront CORS headers, OPTIONS preflight requests, and presigned POST uploads.
The Error
The browser blocks a request to an S3 bucket:
Access to fetch at 'https://mybucket.s3.amazonaws.com/image.jpg'
from origin 'https://myapp.example.com' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.Or a file upload using a pre-signed URL fails:
Access to XMLHttpRequest at 'https://mybucket.s3.amazonaws.com/uploads/...'
from origin 'https://myapp.example.com' 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.Or CORS works for GET requests but fails for PUT (file upload):
Method PUT is not allowed by Access-Control-Allow-Methods in preflight response.Why This Happens
S3 buckets don’t serve CORS headers by default. Browsers send a preflight OPTIONS request before cross-origin PUT, POST, or credentialed GET requests. If S3 doesn’t respond with the correct CORS headers, the browser blocks the actual request.
S3’s CORS is separate from:
- Bucket policies — control who can access the bucket, not CORS headers
- ACLs — control object access, not CORS headers
- CloudFront — CloudFront has its own CORS configuration that must align with S3’s
Common mistakes:
- Missing CORS configuration on the bucket — the most common cause
- AllowedOrigins doesn’t match the request origin —
*works for non-credentialed requests; credentialed requests need an explicit origin - AllowedMethods missing PUT — GET-only CORS configuration blocks upload requests
- AllowedHeaders missing custom headers —
Content-Type,x-amz-*headers must be explicitly allowed - CloudFront caching the
OPTIONSresponse — CloudFront may cache an old response that lacks CORS headers
Fix 1: Add CORS Configuration to the S3 Bucket
Configure CORS on the S3 bucket via the AWS console or CLI:
AWS Console:
- Open S3 → select your bucket
- Permissions tab → Cross-origin resource sharing (CORS) → Edit
- Paste the JSON configuration below → Save changes
JSON CORS configuration (permissive — for development):
[
{
"AllowedHeaders": ["*"],
"AllowedMethods": ["GET", "PUT", "POST", "DELETE", "HEAD"],
"AllowedOrigins": ["*"],
"ExposeHeaders": ["ETag", "x-amz-request-id"],
"MaxAgeSeconds": 3000
}
]Production CORS configuration (restrict to specific origins):
[
{
"AllowedHeaders": [
"Content-Type",
"Authorization",
"x-amz-date",
"x-amz-content-sha256",
"x-amz-security-token"
],
"AllowedMethods": ["GET", "PUT", "POST"],
"AllowedOrigins": [
"https://myapp.example.com",
"https://staging.example.com"
],
"ExposeHeaders": ["ETag"],
"MaxAgeSeconds": 3600
}
]Apply via AWS CLI:
# Set CORS configuration
aws s3api put-bucket-cors \
--bucket mybucket \
--cors-configuration file://cors.json
# Verify
aws s3api get-bucket-cors --bucket mybucket
# cors.json example:
cat > cors.json << 'EOF'
{
"CORSRules": [
{
"AllowedHeaders": ["*"],
"AllowedMethods": ["GET", "PUT", "POST", "DELETE", "HEAD"],
"AllowedOrigins": ["https://myapp.example.com"],
"ExposeHeaders": ["ETag"],
"MaxAgeSeconds": 3000
}
]
}
EOFFix 2: Fix Pre-signed URL CORS for File Uploads
Pre-signed URL uploads (direct browser-to-S3) require specific CORS configuration and careful header handling:
// Backend — generate a pre-signed URL for PUT upload
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
const s3 = new S3Client({ region: 'us-east-1' });
async function getUploadUrl(key: string, contentType: string): Promise<string> {
const command = new PutObjectCommand({
Bucket: process.env.S3_BUCKET,
Key: key,
ContentType: contentType,
// ACL: 'public-read', // Only if using ACLs (Object Ownership must allow)
});
return getSignedUrl(s3, command, { expiresIn: 3600 });
}// Frontend — upload using the pre-signed URL
async function uploadFile(file, uploadUrl) {
const response = await fetch(uploadUrl, {
method: 'PUT',
body: file,
headers: {
'Content-Type': file.type, // Must match what was signed
},
// Do NOT set Authorization header — it's embedded in the URL
});
if (!response.ok) {
throw new Error(`Upload failed: ${response.status}`);
}
}S3 CORS config for pre-signed PUT uploads:
[
{
"AllowedHeaders": ["Content-Type", "Content-Length"],
"AllowedMethods": ["PUT"],
"AllowedOrigins": ["https://myapp.example.com"],
"ExposeHeaders": ["ETag"],
"MaxAgeSeconds": 3600
}
]Pre-signed POST for more control (multi-part, file size limits):
// Backend — generate a pre-signed POST
import { createPresignedPost } from '@aws-sdk/s3-presigned-post';
async function getUploadPost(key: string): Promise<{url: string; fields: Record<string, string>}> {
const { url, fields } = await createPresignedPost(s3, {
Bucket: process.env.S3_BUCKET,
Key: key,
Conditions: [
['content-length-range', 0, 10 * 1024 * 1024], // Max 10MB
['starts-with', '$Content-Type', 'image/'],
],
Fields: {
'Content-Type': 'image/jpeg',
},
Expires: 600, // 10 minutes
});
return { url, fields };
}// Frontend — upload with FormData (pre-signed POST)
async function uploadWithPost(file, { url, fields }) {
const formData = new FormData();
// Append all policy fields FIRST
Object.entries(fields).forEach(([key, value]) => {
formData.append(key, value);
});
// File must be the LAST field
formData.append('file', file);
const response = await fetch(url, {
method: 'POST',
body: formData,
// Do NOT set Content-Type header — browser sets it with the boundary
});
if (!response.ok) {
throw new Error(`Upload failed: ${response.status} ${await response.text()}`);
}
}CORS config for pre-signed POST:
[
{
"AllowedHeaders": ["*"],
"AllowedMethods": ["POST"],
"AllowedOrigins": ["https://myapp.example.com"],
"ExposeHeaders": [],
"MaxAgeSeconds": 3600
}
]Fix 3: Fix CloudFront CORS
If your S3 bucket is served through CloudFront, you need additional configuration. CloudFront must forward the Origin request header to S3 and pass CORS response headers to the browser:
Create a Cache Policy that forwards the Origin header:
# Option 1 — Use CloudFront's managed "CORS-S3Origin" cache policy
# Policy ID: 88a5eaf4-2fd4-4709-b370-b4c650ea3fcf
# Apply it to your CloudFront distribution's behaviors
# Option 2 — Create a custom policy via AWS console or CLI
aws cloudfront create-cache-policy --cache-policy-config '{
"Name": "CORS-Policy",
"DefaultTTL": 86400,
"MaxTTL": 31536000,
"MinTTL": 0,
"ParametersInCacheKeyAndForwardedToOrigin": {
"EnableAcceptEncodingGzip": true,
"HeadersConfig": {
"HeaderBehavior": "whitelist",
"Headers": {
"Quantity": 1,
"Items": ["Origin"]
}
},
"CookiesConfig": { "CookieBehavior": "none" },
"QueryStringsConfig": { "QueryStringBehavior": "none" }
}
}'CloudFront Origin Request Policy — forward headers to S3:
# Create an Origin Request Policy that passes Access-Control headers
# Use managed "CORS-S3Origin" policy or create customIn AWS Console:
- CloudFront → Distributions → select your distribution
- Behaviors tab → edit the behavior serving your S3 content
- Cache key and origin requests → select “Cache policy and origin request policy”
- Cache policy: Select
CachingOptimized - Origin request policy: Select
CORS-S3Origin(AWS managed)
Invalidate the CloudFront cache after fixing:
aws cloudfront create-invalidation \
--distribution-id YOUR_DISTRIBUTION_ID \
--paths "/*"
# Wait for invalidation to complete before testing againFix 4: Debug CORS Issues
Systematic approach to diagnosing CORS problems:
# Step 1 — Send an OPTIONS preflight request manually
curl -X OPTIONS \
"https://mybucket.s3.amazonaws.com/test-file.jpg" \
-H "Origin: https://myapp.example.com" \
-H "Access-Control-Request-Method: GET" \
-H "Access-Control-Request-Headers: Content-Type" \
-v 2>&1 | grep -i "access-control\|HTTP/"
# Expected response headers:
# Access-Control-Allow-Origin: https://myapp.example.com
# Access-Control-Allow-Methods: GET, PUT, POST
# Access-Control-Allow-Headers: Content-Type
# Access-Control-Max-Age: 3000
# If you get no Access-Control-* headers → CORS not configured on the bucket
# Step 2 — Test a direct GET request
curl "https://mybucket.s3.amazonaws.com/test-file.jpg" \
-H "Origin: https://myapp.example.com" \
-v 2>&1 | grep -i "access-control"
# Step 3 — Check the bucket's CORS configuration
aws s3api get-bucket-cors --bucket mybucket
# "NoSuchCORSConfiguration" → bucket has no CORS config → add oneBrowser DevTools — inspect the preflight:
- Open DevTools → Network tab
- Filter by the file or URL
- Look for the
OPTIONSrequest (preflight) before the actual request - Check its response headers for
Access-Control-*headers - If the OPTIONS request returns 403 or has no CORS headers, the bucket policy or CORS config is wrong
Fix 5: Fix CORS for Authenticated Requests
If your S3 requests include credentials (cookies or Authorization headers), AllowedOrigins: ["*"] won’t work. Browsers require an explicit origin for credentialed cross-origin requests:
// WRONG — wildcard origin with credentials
[
{
"AllowedOrigins": ["*"], // Browsers reject credentialed requests with wildcard
"AllowedMethods": ["GET"],
"AllowedHeaders": ["Authorization"]
}
]
// CORRECT — explicit origin for credentialed requests
[
{
"AllowedOrigins": ["https://myapp.example.com"],
"AllowedMethods": ["GET", "PUT"],
"AllowedHeaders": ["Authorization", "Content-Type"]
}
]// Frontend — if using credentials in the request
fetch('https://mybucket.s3.amazonaws.com/file.jpg', {
credentials: 'include', // Sends cookies — requires explicit AllowedOrigins
// Note: S3 pre-signed URLs typically don't use cookies
// Only use credentials: 'include' if your bucket requires it
});Fix 6: Terraform and CDK — Configure CORS Programmatically
If managing AWS infrastructure as code:
Terraform:
resource "aws_s3_bucket_cors_configuration" "uploads" {
bucket = aws_s3_bucket.uploads.id
cors_rule {
allowed_headers = ["Content-Type", "Authorization"]
allowed_methods = ["GET", "PUT", "POST"]
allowed_origins = [
"https://myapp.example.com",
"https://staging.example.com",
]
expose_headers = ["ETag"]
max_age_seconds = 3600
}
}AWS CDK (TypeScript):
import { Bucket } from 'aws-cdk-lib/aws-s3';
import { HttpMethods } from 'aws-cdk-lib/aws-s3';
const bucket = new Bucket(this, 'UploadsBucket', {
cors: [
{
allowedHeaders: ['Content-Type', 'Authorization'],
allowedMethods: [HttpMethods.GET, HttpMethods.PUT, HttpMethods.POST],
allowedOrigins: ['https://myapp.example.com'],
exposedHeaders: ['ETag'],
maxAge: 3600,
},
],
});Still Not Working?
CORS config applied but still failing — S3 CORS changes take effect immediately, but CloudFront caches responses. Invalidate the CloudFront cache after any CORS changes.
Bucket policy blocking CORS — even with CORS configured, a bucket policy that denies the request will cause a 403 before CORS headers are evaluated. Check the bucket policy:
aws s3api get-bucket-policy --bucket mybucket
# Look for explicit Deny statements that might block your requestObject-level ACLs — if the object doesn’t have the correct ACL (or Object Ownership is set to “Bucket owner enforced”), access is denied regardless of CORS config.
ExposeHeaders for client access — response headers are only accessible to JavaScript if they’re in ExposeHeaders. If your frontend needs to read ETag or custom headers, add them:
{
"ExposeHeaders": ["ETag", "x-amz-version-id", "x-amz-request-id"]
}Development with localhost — for local development, include http://localhost:3000 in AllowedOrigins. Browsers treat http://localhost:3000 and https://localhost:3000 as different origins:
{
"AllowedOrigins": [
"https://myapp.example.com",
"http://localhost:3000",
"http://localhost:5173"
]
}For related AWS issues, see Fix: AWS IAM Permission Denied and Fix: GitHub Actions Docker Build Push.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: AWS Lambda Layer Not Working — Module Not Found or Layer Not Applied
How to fix AWS Lambda Layer issues — directory structure, runtime compatibility, layer ARN configuration, dependency conflicts, size limits, and container image alternatives.
Fix: AWS SQS Not Working — Messages Not Received, Duplicate Processing, or DLQ Filling Up
How to fix AWS SQS issues — visibility timeout, message not delivered, duplicate messages, Dead Letter Queue configuration, FIFO queue ordering, and Lambda trigger problems.
Fix: AWS Access Denied — IAM Permission Errors and Policy Debugging
How to fix AWS Access Denied errors — understanding IAM policies, using IAM policy simulator, fixing AssumeRole errors, resource-based policies, and SCPs blocking actions.
Fix: AWS ECS Task Failed to Start
How to fix ECS tasks that fail to start — port binding errors, missing IAM permissions, Secrets Manager access, essential container exit codes, and health check failures.