Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Fix an S3 image CORS error by configuring the bucket to allow the exact origin that serves your page and the method and request headers used by JavaScript. In the S3 console, open Permissions → Cross-origin resource sharing (CORS) → Edit, save a matching JSON rule, then verify the browser’s actual request in DevTools. CORS does not grant access to an object: ACLs, bucket policies, Block Public Access settings and other permissions still apply.

For a basic JavaScript GET, start with a narrow rule for your real scheme and hostname rather than adding a wildcard blindly:

What an S3 CORS error actually means

Cross-origin resource sharing (CORS) is the browser permission mechanism that lets a page loaded from one origin request a resource from another origin. An origin is the combination of scheme, hostname and port. Thus, https://example.com, https://www.example.com and http://example.com are different origins.

When JavaScript requests an S3 object, the browser sends the page’s origin in an Origin request header. S3 chooses the first CORS rule that matches the request. The origin, HTTP method and, when applicable, every header named in Access-Control-Request-Headers must satisfy that rule. If they do not, the browser reports a CORS failure even when the object URL appears correct.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

CORS is separate from authorization. AWS states: “When you enable CORS on the bucket, the access control lists (ACLs) and other access permission policies continue to apply.” A CORS rule cannot make a private object readable, override a bucket policy, or repair a wrong object key.

Use this sequence to find the mismatch

  1. Open the failing page in the browser. Open Developer Tools, select Network, reload the page and select the failed S3 request.
  2. Record the request details. Write down the exact object URL, the page’s Origin, HTTP method, status, whether an OPTIONS request appeared, Access-Control-Request-Method, Access-Control-Request-Headers, and the response’s Access-Control-Allow-Origin and related headers.
  3. Compare each value with the bucket rule. The page origin must be allowed exactly, the method must be listed, and every requested header must be covered when a preflight occurs.
  4. Check access independently. Open the object URL directly or inspect its status. A 403 or 404 is an object-access or URL problem, not something CORS alone can fix.
  5. Retest after saving the bucket configuration. Clear any relevant browser or proxy cache and repeat the same request. Do not change several unrelated settings at once; otherwise you will not know which mismatch was corrected.

Configure the S3 bucket in the console

  1. Sign in to AWS and open S3.
  2. Select the bucket that contains the image.
  3. Open the Permissions tab.
  4. Find Cross-origin resource sharing (CORS) and choose Edit.
  5. Enter a valid JSON configuration and save it. The S3 console requires JSON, not YAML or an XML-style policy.

Minimal rule for a JavaScript image GET

[
  {
    "AllowedOrigins": ["https://www.example.com"],
    "AllowedMethods": ["GET", "HEAD"],
    "AllowedHeaders": []
  }
]

Replace https://www.example.com with the exact scheme and hostname that serves your page. If the page is served from a development port, include that port in the origin. The example allows GET and HEAD; keep HEAD only if your client, library or proxy actually sends it. S3 accepts GET, PUT, POST, DELETE and HEAD in CORS rules.

When request headers require a preflight

A simple image request may not trigger preflight. JavaScript that sends non-simple methods or custom request headers normally causes the browser to send OPTIONS first. S3 then checks the requested method and the names in Access-Control-Request-Headers. Add those actual header names to AllowedHeaders; do not guess at names and do not confuse this setting with response headers. If the preflight asks for a header that the rule does not allow, S3 can omit the CORS response headers and the browser blocks the follow-up request.

When JavaScript must read response metadata

AllowedHeaders describes headers the browser intends to send. ExposeHeaders describes response headers that JavaScript is permitted to read. Add only the response headers your code needs, such as a custom S3 metadata header. You generally do not need ExposeHeaders merely to display image pixels.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

JavaScript examples that make the request visible

Fetch an image and create a browser URL

const objectUrl = 'https://BUCKET.s3.REGION.amazonaws.com/path/image.jpg';

async function loadImage() {
  const response = await fetch(objectUrl, {
    method: 'GET'
  });

  if (!response.ok) {
    throw new Error(`S3 returned ${response.status}`);
  }

  const blob = await response.blob();
  const img = document.querySelector('#photo');
  img.src = URL.createObjectURL(blob);
}

loadImage().catch(console.error);

mode: 'cors' can make your intent explicit, but it cannot grant permission. The S3 response must still contain an Access-Control-Allow-Origin value that matches the page. If your code adds headers, use the Network panel to see whether that changes the request into a preflighted request.

Use an image element when you do not need JavaScript to read the response

<img id="photo"
     src="https://BUCKET.s3.REGION.amazonaws.com/path/image.jpg"
     alt="Product photo">

Displaying an image and reading its bytes are different browser operations. If your application fetches the bytes, draws the image to a canvas and reads pixels, configure S3 CORS and set the appropriate crossorigin behavior before the image is requested. A CORS rule still does not change whether the object itself is publicly readable or authorized for the requesting user.

Test the preflight outside the browser

Use the exact object URL and page origin from DevTools. This test models a browser preflight for a GET:

curl -i -X OPTIONS 
  -H 'Origin: https://www.example.com' 
  -H 'Access-Control-Request-Method: GET' 
  'https://BUCKET.s3.REGION.amazonaws.com/OBJECT'

If the browser included Access-Control-Request-Headers, add the same header to the curl command. A matching S3 example returns 200 OK with Access-Control-Allow-Origin and allowed-method information. Treat that status as a diagnostic result, not proof that the browser’s request is identical: compare the URL, origin and requested headers character for character.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Diagnose the common failure patterns

What you observe What to check Likely correction
S3 reports that CORS is not enabled Whether the bucket has any valid CORS configuration Add and save a valid bucket CORS rule. This does not grant object-read permission.
The response says the request is not allowed The actual page Origin versus AllowedOrigins Add the exact intended origin or correct the spelling, scheme, hostname or port.
GET or HEAD does not match The method shown in the Network panel Allow the method the browser or library really sends.
OPTIONS fails after custom headers are added Access-Control-Request-Headers versus AllowedHeaders Allow every required request-header name, or remove headers your client does not need.
The image loads, but script cannot inspect metadata The response header your code tries to read Expose only that response header with ExposeHeaders.
The bucket rule looks correct, but headers are missing or stale Any CloudFront or other proxy in front of S3 Review OPTIONS handling, forwarded CORS request headers and the proxy cache key.

CloudFront and other proxy checks

A correct bucket rule can still be hidden by an intervening proxy. Ensure the proxy permits OPTIONS, forwards Origin, Access-Control-Request-Method and Access-Control-Request-Headers when required, and does not reuse a response generated for one origin for a request from another. Origin-aware caching is important: a cached response without the right CORS headers can make one browser origin appear to work while another fails.

Also verify which hostname the browser calls. If the page uses a CloudFront distribution, a custom domain or a redirecting URL, configure and test the hostname in the actual request path. Testing the S3 regional endpoint while production uses a CDN can hide a forwarding or cache problem.

Production practices that prevent recurring CORS errors

  • Prefer explicit origins. A production rule should name the application origins that need access. A wildcard can be useful for a deliberately public use case, but it can also allow unintended sites to read responses.
  • Keep methods narrow. For image retrieval, start with GET and add HEAD only when observed. Add write methods only for clients that actually upload or modify objects.
  • Avoid unnecessary custom headers. Fewer non-simple headers mean fewer preflight conditions to maintain. When a header is required, copy its exact name from the browser’s preflight.
  • Separate CORS from authorization reviews. After changing CORS, still check bucket policy, object ownership, ACL behavior, signed URLs and public-access blocks according to your access design.
  • Keep a reproducible test. Save the object URL, page origin and curl preflight command used during an incident. This makes changes to S3 and a CDN independently verifiable.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

If your actual goal is a rendered screenshot or PDF of a page—not JavaScript access to the original S3 bytes—ScreenshotNeo can capture the URL with one request. Its API accepts the page URL and returns PNG, JPEG, WebP or PDF; it is not a replacement for a bucket CORS rule when your own browser code must fetch pixels.

For example, capture a page that displays the image:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

See the ScreenshotNeo API documentation for parameters and response details. Before capture, it accepts cookie or consent banners and removes more than 60 known consent platforms, newsletter popups and chat widgets; each cleanup step can be disabled. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads and cache hits are not billed, and the response identifies the page verdict and billing result in X-Page-Verdict and X-Billed headers. An MCP server provides take_screenshot, get_page_info and capture_pdf tools for Claude, Cursor and other MCP clients. The free plan includes 1,000 screenshots per month without a card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.

Frequently Asked Questions

Why can a direct S3 URL work while JavaScript fails?

A direct navigation and a script request are subject to different browser checks. JavaScript needs a matching CORS response, while direct navigation mainly tests whether the URL can be retrieved.

Should I add every possible method and header to make the error disappear?

No. Match the request you actually observe. A narrow origin, method and header set is easier to review and avoids granting unnecessary cross-origin access.

The Bottom Line

Match the bucket CORS rule to the browser’s exact origin, method and preflight headers, then verify object permissions and any proxy in front of S3. CORS enables the browser check; it does not grant access by itself.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.