GuidesHTML2CANVAS

Fix html2canvas cross-origin images

html2canvas 1.4.1 skips every cross-origin image unless you set one of 3 options: useCORS: true works only when the image host sends Access-Control-Allow-Origin, allowTaint: true draws the image but makes toDataURL() throw a SecurityError, and proxy downloads the image as a blob through a proxy you name. On 28 October 2026 a Google logo without CORS headers rendered and exported in Chromium 153, Firefox 155 and WebKit 26.6 only with proxy: 'https://proxy.cors.dev/', which the free cors.dev proxy serves with no signup, at about 600 requests a minute per IP and images up to 6 MiB.

Why html2canvas drops cross-origin images

html2canvas doesn't take a screenshot. It redraws the DOM onto a canvas, so every image goes through the canvas API, and a cross-origin image drawn without CORS approval taints the canvas. A tainted canvas can't be read or exported: toDataURL(), toBlob() and getImageData() throw a SecurityError.

html2canvas 1.4.1, the current npm release, avoids that by default. Its defaults are useCORS: false, allowTaint: false, proxy: null, imageTimeout: 15000 and logging: true, and its docs describe proxy as "Url to the proxy which is to be used for loading cross-origin images. If left empty, cross-origin images won't be loaded."

The source shows the rule. For each image URL, Cache.loadImage checks whether the image is same-origin, whether useCORS applies (option on, image cross-origin, not a data: URL) and whether the proxy applies (proxy is a string, image cross-origin, not a data: or blob: URL, useCORS off). A cross-origin image that matches neither, with allowTaint off, is skipped without a request. With logging on, html2canvas printed no error for it in any of the 3 browsers tested.

What useCORS, allowTaint and proxy do

The test on 28 October 2026 ran html2canvas 1.4.1 in Chromium 153, Firefox 155 and WebKit 26.6 (Playwright 1.63). A page on http://127.0.0.1:8001 rendered a 600 by 220 pixel card holding the Google logo, a 13,504-byte PNG served without Access-Control-Allow-Origin. All 3 engines gave the same results.

html2canvas 1.4.1 options with an image that sends no CORS headers, checked October 2026
OptionsHow html2canvas loads the imageImage in the outputtoDataURL()
DefaultsSkipped, no requestNoWorks
useCORS trueimg with crossOrigin anonymousNo, CORS error in consoleWorks
allowTaint trueimg without CORSYesThrows SecurityError
proxy setXHR to the proxy, then a data URLYesWorks
useCORS true and proxy setimg with crossOrigin anonymous, proxy skippedNo, CORS error in consoleWorks

The last row is the trap: the proxy path runs only when useCORS is off, so with both set html2canvas loads the image with crossOrigin = 'anonymous' and never calls the proxy.

When the useCORS load fails, each engine logs its own message:

  • Chromium 153: Access to image at 'https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png' from origin 'http://127.0.0.1:8001' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
  • Firefox 155: Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png. (Reason: CORS header 'Access-Control-Allow-Origin' missing). Status code: 200.
  • WebKit 26.6: Cannot load image https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png due to access control checks.

With allowTaint: true the logo is drawn and the export fails. Chromium 153 throws SecurityError: Failed to execute 'toDataURL' on 'HTMLCanvasElement': Tainted canvases may not be exported. Firefox 155 and WebKit 26.6 throw SecurityError: The operation is insecure.

How the proxy option builds its request

Cache.proxy in html2canvas 1.4.1 sends an XMLHttpRequest GET to proxy + '?url=' + encodeURIComponent(src) + '&responseType=blob', using & instead of ? when the proxy string already contains ?. It sets xhr.responseType = 'blob' and xhr.timeout to imageTimeout.

Only status 200 counts. Any other status rejects with Failed to proxy resource <url> with status code <status>, a timeout rejects with Timed out (15000ms) proxying <url> at the default setting, and the renderer logs Error loading image <url> and leaves that image out while the rest renders.

On 200, a FileReader turns the blob into a data: URL and html2canvas draws the image from it, which doesn't taint the canvas. So the proxy has to:

  • accept ?url= followed by a percent-encoded URL
  • return the raw image bytes with status 200
  • send Access-Control-Allow-Origin, because the XHR is itself a cross-origin request

Use cors.dev as the html2canvas proxy

The /?url= alias of the free cors.dev proxy matches that contract. The target may be percent-encoded and anything after a percent-encoded target is ignored, so the &responseType=blob that html2canvas appends does no harm. Set proxy to the base URL and leave useCORS unset:

html2canvas with the cors.dev proxy, exported as a PNG blob
import html2canvas from 'html2canvas';

async function captureToBlob(element) {
  const canvas = await html2canvas(element, {
    // Leave useCORS unset: when it is true, html2canvas skips the proxy
    proxy: 'https://proxy.cors.dev/',
    imageTimeout: 15000,
  });

  return new Promise((resolve, reject) => {
    canvas.toBlob((blob) => {
      if (blob) resolve(blob);
      else reject(new Error('Canvas export failed'));
    }, 'image/png');
  });
}

const blob = await captureToBlob(document.querySelector('#card'));
const previewUrl = URL.createObjectURL(blob);

Probed on 28 October 2026 with an Origin header, https://proxy.cors.dev/?url=<encoded logo URL>&responseType=blob returned 200, image/png and 13,504 bytes, with Access-Control-Allow-Origin echoing the page origin and X-Cors-Source: upstream. In the browser test, the PNG exported by this snippet contained the logo in all 3 engines.

  • Pass the base URL. https://proxy.cors.dev/ and https://proxy.cors.dev both returned the image, while https://proxy.cors.dev/?url= makes html2canvas request ?url=&url=..., which cors.dev answers with 400 and X-Cors-Error: invalid_request.
  • Leave useCORS unset or false, or the proxy is never called.
  • Expect one proxy request per distinct cross-origin image URL on each html2canvas() call, because every call starts with a fresh image cache.
  • Keep images under 6 MiB (6,291,456 bytes). Larger responses fail with 502 and X-Cors-Error: response_too_large, and html2canvas leaves that image out.

The free proxy needs no signup or key and allows about 600 requests a minute per IP. It relays images only to requests that carry an Origin header, which browsers add to html2canvas's XHR automatically.

When useCORS is the better fix

If you control the image host, such as your own CDN or an S3 or R2 bucket, send Access-Control-Allow-Origin there and use useCORS: true, which skips the hop through a proxy. For S3, see AWS S3 CORS.

Some public hosts already send the header. On 28 October 2026 avatars.githubusercontent.com, images.unsplash.com and picsum.photos answered with Access-Control-Allow-Origin: *, so useCORS: true is enough for their images.

A host that sends the header only when the request carries an Origin, without Vary: Origin, can still fail in Chrome when the page loaded the same image earlier without CORS. The tainted canvas guide covers that cache trap.

Good questions.

Why does curl get a 502 from the proxy URL that works in html2canvas?

The free cors.dev proxy relays images and other binary types only to requests that carry an Origin header. Browsers add Origin to html2canvas's cross-origin XHR automatically, but curl sends none, so the same URL answers 502 with X-Cors-Error: unsupported_response. Add -H 'Origin: https://example.com' to test from a terminal.

What happens when one proxied image fails to load?

html2canvas accepts only status 200 from the proxy. Any other status, or a timeout after imageTimeout (15000 ms by default), is logged as Error loading image <url>, and that image is left out while the rest of the element renders. In the 28 October 2026 test, an image that answered 404 through the proxy was skipped this way and the export still worked.

Does the html2canvas proxy option handle CSS background images?

Yes. In the 28 October 2026 test in Chromium 153, Firefox 155 and WebKit 26.6, a cross-origin CSS background-image went through the proxy and was drawn onto the canvas like an img element.

Why is the image still missing with both useCORS and proxy set?

In html2canvas 1.4.1 the proxy path runs only when useCORS is off. With useCORS: true, html2canvas loads the image with crossOrigin = 'anonymous', and a host without Access-Control-Allow-Origin blocks it. Remove useCORS so the proxy handles the image.

How many images can I send through cors.dev?

The free proxy needs no signup or key, accepts GET and HEAD, allows about 600 requests a minute per IP and relays responses up to 6 MiB (6,291,456 bytes) within a 30-second upstream deadline. cors.dev Pro costs $5 a month for 500,000 requests, with a 7-day trial of 1,000 requests and no card, and allows 3,000 requests a minute per account and 100 in flight. The 6 MiB and 30-second limits also apply on Pro.