# The image loads, but canvas export fails: fix image CORS

> Debug a tainted canvas with a two-origin example: check crossorigin, response headers, redirects and cached images without a public proxy.

Canonical page: https://www.imageguide.dev/guides/canvas-image-cors/

Published: 2026-09-05. Updated: 2026-10-17.

Publisher: ImageGuide, published by Sirv. [Editorial method](https://www.imageguide.dev/about/#how-we-write).

## Quick answer

Canvas export fails after drawing a cross-origin image unless its server grants CORS access and the image request uses the matching crossOrigin setting. Set crossOrigin before src, configure the response headers, then reload the image. Being able to display an image does not grant permission to read its pixels.

Reference for this answer: [MDN canvas image guidance](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Using_images).

The image appears on the page. You draw it into a canvas. Then `toDataURL()` throws a `SecurityError`.

Displaying an image from another origin does not automatically give your page permission to read its pixels. The browser keeps those operations separate. [MDN's canvas CORS guide](https://developer.mozilla.org/en-US/docs/Web/HTML/How_to/CORS_enabled_image) explains the permission required for export.

The fix has two parts: request the image in CORS mode and have its server allow your origin. Changing only the JavaScript leaves half the work undone.

## Identify the two origins

An origin includes the scheme, hostname and port. These are different origins:

```text
Page:   http://127.0.0.1:8787
Image:  http://127.0.0.1:8788/image.png
```

The hostname matches, but the ports do not. A production page and its image CDN usually differ by hostname instead.

Write down the page origin and the final image URL before editing anything. In DevTools, keep the Network panel open and select the image request. A request that redirects to another host must be checked through to its final response.

## Compare three cases

| Image request | Server permission | Expected result |
| --- | --- | --- |
| Ordinary image request | No CORS header | Image can display, but drawing it taints the canvas and blocks export |
| `crossOrigin = 'anonymous'` | No matching CORS header | The image load fails |
| `crossOrigin = 'anonymous'` | Header allows the page origin | The image can load and be exported from canvas |

The table describes the browser security model. Error wording differs between browsers. Other restrictions, including a page's Content Security Policy, can block a request before it reaches these cases.

Download the dependency-free [two-origin demonstration](/experiments/2026-09/cors-demo.mjs), save it as `cors-demo.mjs` and run:

```bash
node cors-demo.mjs
```

Open `http://127.0.0.1:8787` and press **Run all three cases**. The page uses the same one-pixel PNG for each case and displays the result below the button. Both servers bind only to loopback. Stop them with Ctrl+C.

The script's `--check` mode verifies both HTTP responses, their headers and the embedded JavaScript syntax. Browser enforcement must be observed by opening the page. We have not presented those HTTP checks as a browser test.

## Set the attribute before the source

```js
const image = new Image();
image.crossOrigin = 'anonymous';
image.src = 'https://images.example.com/product.jpg';
await image.decode();

const canvas = document.createElement('canvas');
canvas.width = image.naturalWidth;
canvas.height = image.naturalHeight;
const context = canvas.getContext('2d');
if (!context) throw new Error('Canvas 2D is unavailable');
context.drawImage(image, 0, 0);

const blob = await new Promise((resolve, reject) => {
  try {
    canvas.toBlob(value => {
      if (value) resolve(value);
      else reject(new Error('The browser could not encode the canvas'));
    }, 'image/png');
  } catch (error) {
    reject(error);
  }
});
```

This is the export core. Call it from your application's error-handled flow. Setting `crossOrigin` before `src` makes the intended request mode explicit before loading starts. `toBlob()` avoids building a large base64 string, but it does not bypass the canvas origin rules.

For markup, the equivalent attribute is `crossorigin="anonymous"`. See the [HTML image element specification](https://html.spec.whatwg.org/multipage/embedded-content.html#the-img-element) for the request behavior.

## Configure the image response

For the local demonstration, the allowed response includes:

```http
Access-Control-Allow-Origin: http://127.0.0.1:8787
```

Use your actual page origin in production. For genuinely public, noncredentialed images, `Access-Control-Allow-Origin: *` can be appropriate. For private assets, validate the origin against an allowlist. If the response varies by `Origin`, include `Vary: Origin` and make sure your CDN respects it.

Do not reflect any incoming origin into a credentialed response. Read the [CORS response-header guidance](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS) before changing private-image access.

A header check can help separate server configuration from browser code:

```bash
curl --location --dump-header - --output /dev/null \
  --header 'Origin: https://shop.example.com' \
  'https://images.example.com/product.jpg'
```

Curl does not enforce browser CORS. It shows the responses so you can inspect them. A successful download in curl proves reachability, not that canvas export will work.

## When the fix appears not to work

Check these in order:

1. Confirm that the request in DevTools actually contains the expected `Origin` header.
2. Inspect every redirect and the final response. Check the final asset host's configuration.
3. Disable the browser cache while testing. An image previously loaded by ordinary markup may have a cached response from before the header change.
4. Check a service worker if the response says it came from one.
5. Load a fresh image in CORS mode and draw it onto a new canvas. Fixing the server does not clean pixels already drawn from an unauthorized source.

Do not use `fetch(..., { mode: 'no-cors' })` as a workaround. It gives JavaScript an opaque response rather than readable image bytes. Do not send private images through an arbitrary public CORS proxy.

If you control neither the image server nor its headers, offer a local file picker or change the export design. Start the next debugging session with the actual request and response, not another `onerror` handler.
