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

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

Cross-origin images disappear in html2canvas because the browser protects canvas pixels. Set useCORS: true only when the image server returns a compatible Access-Control-Allow-Origin header. If you cannot change that server, fetch the image through a carefully restricted same-origin proxy and pass its URL with proxy. Do not rely on allowTaint: true: it can draw the image but leaves the canvas unreadable for export.

What the error means

Browsers treat a page origin as the combination of scheme, host, and port. An image from another origin can usually be displayed in an <img>, but drawing it into a canvas changes the security rules. If the response does not grant cross-origin access, the canvas becomes tainted. Scripts then cannot read pixels or call export methods such as toDataURL(), toBlob(), or libraries that depend on them.

html2canvas follows those browser rules; it does not bypass them. With its default allowTaint: false, it checks whether a resource would taint the canvas and skips the image instead. That is why a remote image may be visible on the page yet missing from the screenshot.

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

First, confirm that it is really a cross-origin failure

  • Compare the image URL and page URL character by character. A different scheme (HTTP versus HTTPS), hostname, or port is cross-origin.
  • Open browser developer tools and inspect the image request. Check redirects, the final status code, and whether the response contains Access-Control-Allow-Origin.
  • Look for ordinary failures too: 404/403 responses, hotlink protection, an expired signed URL, mixed-content blocking, or an image that has not finished loading.
  • Use the html2canvas onError callback to expose failed image, SVG, or background-image resources.
const element = document.querySelector('#capture');

await html2canvas(element, {
  useCORS: true,
  onError: error => console.warn('html2canvas resource failed:', error.message)
});

Capture after the images have completed loading. A race between page rendering and capture can look like a CORS problem even when the headers are correct.

Fix 1: configure the image server you control

1. Return the right CORS header

Configure the endpoint that serves the image to return Access-Control-Allow-Origin for the page origin, for example:

Access-Control-Allow-Origin: https://app.example.com

Use * only when the asset and request model allow public, credential-free access. If requests include cookies or other credentials, a wildcard origin is not compatible; return the specific requesting origin and configure credentials deliberately.

2. Ask html2canvas to use CORS

const canvas = await html2canvas(document.querySelector('#capture'), {
  useCORS: true
});

const png = canvas.toDataURL('image/png');

useCORS defaults to false. Turning it on changes how html2canvas requests eligible images; it cannot manufacture permission that the remote response does not provide.

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

3. Set the image request mode before loading

When your code creates the image element, set crossorigin before assigning src:

const image = new Image();
image.crossOrigin = 'anonymous';
image.src = 'https://images.example.com/photo.jpg';
await image.decode();
document.querySelector('#capture').append(image);

In markup, the equivalent is:

<img crossorigin="anonymous" src="https://images.example.com/photo.jpg" alt="">

The request mode and response header must agree. Adding the attribute after the browser has already requested the image is too late. If an image was first loaded without CORS, replace it with a new CORS-enabled request before capture.

Fix 2: use a same-origin proxy when you do not control the host

A proxy on your own origin retrieves the remote resource and serves it back from a URL your page can read. Pass that endpoint to html2canvas:

const canvas = await html2canvas(document.querySelector('#capture'), {
  proxy: '/image-proxy'
});

The proxy pattern commonly accepts a ?url= parameter, fetches the target, and returns the image (or a data URI). A minimal Express-style example is:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
import express from 'express';
import dns from 'node:dns/promises';

const app = express();
const allowedHosts = new Set(['images.example.net', 'cdn.example.org']);

app.get('/image-proxy', async (req, res) => {
  try {
    const target = new URL(String(req.query.url || ''));
    if (target.protocol !== 'https:' || !allowedHosts.has(target.hostname)) {
      return res.status(400).send('URL not allowed');
    }

    const upstream = await fetch(target, { redirect: 'manual' });
    if (!upstream.ok) return res.status(502).send('Upstream image failed');
    const type = upstream.headers.get('content-type') || '';
    const length = Number(upstream.headers.get('content-length') || 0);
    if (!type.startsWith('image/') || length > 10_000_000) {
      return res.status(415).send('Unsupported image');
    }

    res.set('Content-Type', type);
    res.set('Cache-Control', 'public, max-age=300');
    res.send(Buffer.from(await upstream.arrayBuffer()));
  } catch {
    res.status(400).send('Invalid image URL');
  }
});

app.listen(3000);

The example deliberately uses an allow-list. A production proxy should validate destination URLs, block private-network and loopback addresses (including after DNS resolution), limit redirects, enforce response-size and content-type limits, apply timeouts and rate limits, and avoid forwarding sensitive headers. Otherwise a caller-supplied URL can turn the endpoint into an open server-side request-forgery relay. These are deployment safeguards, not html2canvas options.

Call the proxy with the encoded target URL:

const proxyUrl = `/image-proxy?url=${encodeURIComponent(
  'https://images.example.net/photo.jpg'
)}`;
const canvas = await html2canvas(document.querySelector('#capture'), {
  proxy: proxyUrl
});

Test the proxy URL directly in a browser or with an HTTP client. It should return the expected image bytes and a correct Content-Type, and the page should be able to load it from its own origin.

Choosing between CORS and a proxy

Question Configure the image host Use a same-origin proxy
Do you control the image server? Yes No, or policy cannot be changed
Extra infrastructure None beyond header configuration Server, validation, monitoring and bandwidth
Credentials Must be coordinated with CORS and cannot use wildcard origins for credentialed requests Can keep upstream credentials on the server; never expose them to the browser
Latency and cost One image request An additional hop and proxy bandwidth; caching can reduce repeat work
Privacy exposure Browser contacts the image host directly Your server sees requested URLs and content
Best fit Stable assets on a host you operate Third-party assets or legacy hosts without suitable headers

Why common “fixes” fail

allowTaint: true

await html2canvas(element, { allowTaint: true });

This permits drawing an image that would taint the canvas. It does not make pixel reads safe. Export operations can still throw a security exception or fail, so it is not an export solution.

Turning on useCORS by itself

The option is necessary for a CORS-enabled response, but it cannot add an Access-Control-Allow-Origin header to a server you do not control. If the header is absent or incompatible, move the asset, change the server policy, or use a proxy.

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

Adding a header in frontend JavaScript

JavaScript cannot add a response header. A service worker or application server can change how a resource is delivered, but the browser still evaluates the actual response and request mode.

Rank #4
HTML5 HTML Logo Web Programmer Nerd Funny - Computer Coding T-Shirt
  • Are you familiar with html5? Then get this "HTML5 HTML Logo Web Programmer Nerd Funny" featuring HTML logo. Perfect for computer programmer, developer, software developer and technician who does computer programming language, coding and gaming on internet.
  • Lightweight, Classic fit, Double-needle sleeve and bottom hem

Assuming iframes behave like images

A cross-origin iframe is a separate limitation. Its contentDocument is inaccessible, so html2canvas cannot inspect and render the embedded document even if the iframe contains CORS-enabled images. Render content you own in the same origin, or capture it from the application that serves it.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Troubleshooting by symptom

The image is skipped with useCORS: true

Inspect the final response after redirects. Confirm the exact page origin is allowed, that the request was made with crossorigin="anonymous", and that the image was not cached from an earlier non-CORS load. Replace the element or add a cache-busting URL while testing.

The proxy returns HTML or a 403

Check authentication, redirect handling, allow-listed hosts, and the upstream status. Verify the proxy sends image bytes rather than an error page and sets the matching image MIME type.

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

Only some images fail

Different CDNs often have different headers, redirects, or hotlink rules. Inspect each failing request, including CSS background-image URLs and SVG references. Make every asset use either the same CORS policy or the proxy.

Best Value
The SQL Programming Language: .
  • Used Book in Good Condition

The screenshot is blank or incomplete

Wait for img.decode() or equivalent load events, then capture. Check that lazy-loaded images have entered the DOM, that CSS backgrounds are reachable, and that a failed font or script is not preventing the page from rendering. Use onError to identify resource failures.

Export throws a security exception

At least one drawn resource tainted the canvas. Remove allowTaint: true, find the offending request in developer tools, and fix its CORS header or route it through your proxy before calling toDataURL or toBlob.

Or skip the browser setup

ScreenshotNeo provides a website screenshot API and MCP server. It accepts consent banners before capture and removes more than 60 known consent platforms, newsletter popups and chat widgets; each cleanup step can be disabled. Bot checks, blank pages, timeouts, failed loads and cache hits are not billed, and response headers identify the page verdict and billing status. AI agents can use its MCP tools—take_screenshot, get_page_info and capture_pdf—from Claude, Cursor or another MCP client.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

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

One GET request returns an image or PDF:

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 documentation for options and authentication. The same request in Python is:

import requests
r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)

And in Node.js:

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
await Bun.write('shot.webp', res);

Every plan includes the capture features: full-page and selector shots, device and viewport controls, retina scale, dark mode, custom CSS/JavaScript, waits, request blocking, headers and cookies, geolocation, PDFs, resizing, caching, signed links, asynchronous webhooks, bulk capture, usage data and an OpenAPI specification. Pricing starts with 1,000 free shots each month without a card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.

Performance, reliability and cost considerations

  • Prefer CORS headers for assets you own: it avoids proxy bandwidth and an extra network hop.
  • Use proxy caching for immutable images, but choose a TTL that matches how quickly assets change.
  • Keep proxy limits explicit. Large images consume memory and delay capture; reject unsupported media early.
  • Capture only after required resources are ready. A long fixed delay is less reliable than waiting for a specific selector or load condition.
  • For repeated server-side screenshots, consider a service designed for capture. ScreenshotNeo bills only clean shots; failed loads and cache hits are identified in response headers and are not billed.

Frequently Asked Questions

Can I fix this entirely in CSS?

No. CSS can change which image is displayed, but it cannot grant the browser permission to read pixels from a response that lacks a compatible CORS policy.

Does converting the image to base64 always solve it?

A data URI is same-origin once created, but obtaining it still requires a permitted fetch or a trusted server-side conversion. Base64 conversion in frontend code cannot bypass CORS.

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

Are SVG images subject to the same problem?

Yes. External SVG resources and images referenced inside SVG can fail or taint the canvas; inspect their requests and apply the same CORS or proxy strategy.

Quick Recap

Bestseller No. 3
Bestseller No. 4
HTML5 HTML Logo Web Programmer Nerd Funny - Computer Coding T-Shirt
HTML5 HTML Logo Web Programmer Nerd Funny - Computer Coding T-Shirt
Lightweight, Classic fit, Double-needle sleeve and bottom hem
$19.99
Bestseller No. 5
The SQL Programming Language: .
The SQL Programming Language: .
Used Book in Good Condition
$4.23

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.