I deployed a web application that fetches images and documents from an S3 bucket, and the browser console exploded with CORS errors. The requests were failing, users couldn’t see images, and I had no idea why the S3 bucket wasn’t responding with the proper headers. After reading a few AWS docs and testing different configurations, I realized CORS wasn’t enabled on the bucket at all—and when I did enable it, I forgot to include the correct HTTP methods. In this post, I’ll walk through exactly what causes this and how to fix it.

The Problem

Your web application makes a cross-origin request to S3 (e.g., https://myapp.com requests from https://my-bucket.s3.amazonaws.com). The browser blocks the request and logs: “Access to fetch at ‘https://my-bucket.s3.amazonaws.com/file.jpg’ from origin ‘https://myapp.com’ has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present”.

Error Type Description
No Access-Control-Allow-Origin Header S3 didn’t return the CORS header, usually because CORS is not configured.
Method Not Allowed in CORS Request uses PUT or DELETE, but CORS config only allows GET.
Preflight Request Failed Browser’s OPTIONS request failed, blocking the actual request.

Why Does This Happen?

  • No CORS configuration on the bucketS3 CORS is disabled by default. Requests from different origins are blocked unless you explicitly configure CORS.
  • CORS config doesn’t include your requesting origin — The CORS rule specifies AllowedOrigins as a list. If your origin (https://myapp.com) isn’t in the list, the request is blocked.
  • CORS doesn’t allow the required HTTP method — Your app uses PUT for presigned uploads, but the CORS config only allows GET. The browser blocks the request before it reaches S3.
  • Wildcard in AllowedOrigins without AllowedHeaders — Using * for origins requires explicit AllowedHeaders to allow all headers, or the request fails.

The Fix

Step 1: Create CORS Configuration

Create a CORS configuration file with the appropriate settings:

cat > cors-config.json <<EOF
{
  "CORSRules": [
    {
      "AllowedOrigins": [
        "https://myapp.com",
        "https://www.myapp.com"
      ],
      "AllowedMethods": [
        "GET",
        "PUT",
        "POST",
        "DELETE",
        "HEAD"
      ],
      "AllowedHeaders": [
        "*"
      ],
      "ExposeHeaders": [
        "ETag",
        "x-amz-version-id"
      ],
      "MaxAgeSeconds": 3000
    }
  ]
}
EOF

Understanding Each Setting

  • AllowedOrigins — The origins allowed to make requests. Use exact domains or * for all (not recommended for production).
  • AllowedMethods — HTTP methods permitted. Use GET for static assets, add PUT and DELETE for uploads/deletions via presigned URLs.
  • AllowedHeaders — Request headers the browser can send. Use * to allow all.
  • ExposeHeaders — Response headers the browser can read. Always include ETag for versioning and upload verification.
  • MaxAgeSeconds — How long the browser caches the preflight (OPTIONS) response. Default is 0; 3000 seconds (50 minutes) is typical.

Step 2: Apply CORS to the Bucket

Apply the CORS configuration to your S3 bucket:

aws s3api put-bucket-cors \
  --bucket my-bucket \
  --cors-configuration file://cors-config.json

Step 3: Verify CORS Configuration

Check the configuration was applied:

aws s3api get-bucket-cors --bucket my-bucket

Step 4: Test CORS with a Browser Request

Test CORS by fetching an object from the browser console:

fetch('https://my-bucket.s3.amazonaws.com/test.jpg')
  .then(response => response.blob())
  .then(blob => console.log('Success:', blob))
  .catch(error => console.error('CORS Error:', error));

If CORS is configured correctly, the request succeeds and you see the blob in the console.

Step 5: For Presigned URLs, Ensure Methods Match

If your application uses presigned URLs for uploads, verify the CORS config includes PUT:

# Generate a presigned POST URL (requires PUT in CORS)
aws s3 presign s3://my-bucket/test.jpg \
  --expires-in 3600 \
  --region us-east-1

How to Run This

  1. Replace https://myapp.com with your actual application domain
  2. Replace my-bucket with your bucket name
  3. Include all HTTP methods your app uses (typically GET, PUT for uploads)
  4. Apply the configuration and test immediately from your browser
  5. Check the browser’s Network tab to confirm the response includes the Access-Control-Allow-Origin header

Is This Safe?

CORS with specific allowed origins is safe. Only list the exact domains that need access. Using "AllowedOrigins": ["*"] is generally acceptable for public content but less secure than explicit domains. For sensitive data, always use presigned URLs with short expiry times and specific ACLs.

Key Takeaway

S3 CORS errors occur when the bucket has no CORS configuration or the configuration doesn’t include your application’s origin and required HTTP methods. Configure CORS once, test from the browser, and your cross-origin requests will work seamlessly.


Have questions or ran into a different S3 issue? Connect with me on LinkedIn or X.