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.
| Options | How html2canvas loads the image | Image in the output | toDataURL() |
|---|---|---|---|
| Defaults | Skipped, no request | No | Works |
| useCORS true | img with crossOrigin anonymous | No, CORS error in console | Works |
| allowTaint true | img without CORS | Yes | Throws SecurityError |
| proxy set | XHR to the proxy, then a data URL | Yes | Works |
| useCORS true and proxy set | img with crossOrigin anonymous, proxy skipped | No, CORS error in console | Works |
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:
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/andhttps://proxy.cors.devboth returned the image, whilehttps://proxy.cors.dev/?url=makes html2canvas request?url=&url=..., which cors.dev answers with400andX-Cors-Error: invalid_request. - Leave
useCORSunset orfalse, 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
502andX-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.