← dompdf.js Studio

Cross-Origin Images and CORS Handling in dompdf.js

Images on the web frequently come from CDNs, image hosts, or third-party services that do not share an origin with the page, and pushing those cross-origin images into a PDF is one of the most common stumbling blocks for dompdf.js developers. The cross-origin situation itself does not stop the image from loading or displaying; the problem appears when the PDF pipeline needs to read the image's pixel data. The browser security model says that scripts may not read the content of cross-origin images unless the server grants permission through CORS, so the render pipeline either gets nothing or gets blocked by the security policy. Understanding the same-origin policy and the CORS mechanism is the prerequisite for solving any of this. This guide starts with the difference between same-origin and cross-origin requests, then walks through the four mainstream solutions: configuring CORS response headers on the image server, fetching the image and converting it to a data URL, routing images through a same-origin proxy, and hosting images on your own origin. Each approach is compared for applicability and cost, because the right answer depends on whether you control the image server. The guide closes with a step-by-step troubleshooting checklist that turns a confusing CORS failure into a ten-minute diagnosis, so cross-origin images stop being a source of blank PDFs and mysterious console errors in your projects. The same authorization patterns apply to fonts and other embedded resources, so the techniques here are reusable beyond images; and the checklist at the end compresses the whole guide into a diagnosis routine you can run in minutes.

The Same-Origin Policy and Image Read Access

The browser's same-origin policy states that a page can only read the content of resources that share its origin, meaning the same protocol, host, and port. A plain img tag can display a cross-origin image without restriction, but scenarios that need to read pixel data — canvas operations and PDF rendering among them — are tainted or blocked when the image has not been authorized. That distinction is the root cause of cross-origin image failures in PDF generation, and once it is clear, the rest of the problem becomes a question of authorization rather than mystery.

Because the dompdf.js pipeline runs inside the browser, it is bound by the same security model: a cross-origin image in your template can display perfectly on the page, yet the PDF generation may still be unable to obtain its data. The solution must therefore either make the image server explicitly authorize the page through CORS response headers, or bypass the cross-origin situation entirely by turning the image into same-origin data first. Both paths are viable, and the choice depends on how much control you have over the image source.

There is a quick experiment that confirms whether CORS is the culprit: replace the image address with a same-origin resource or a data URL, and if the PDF immediately works, the problem is almost certainly CORS. Conversely, if a same-origin image also fails, the issue is more likely a bad path, an unsupported format, or a loading-timing problem. Running this control experiment first saves a great deal of time by steering you away from the wrong layer of the stack.

It is also worth remembering that the same-origin policy applies to fonts and other embedded resources with the same force. A font that loads fine in the browser can silently fall back in the PDF if its CORS headers are missing, which is why the authorization patterns in this guide apply beyond images and are worth learning once and reusing everywhere.

CORS Response Headers and Server Configuration

The standard way to make a cross-origin image readable is to configure CORS response headers on the server that serves it. The core header is Access-Control-Allow-Origin, which declares which origins may read the resource. A value of * means any origin can read it, which is fine for public CDNs and open image hosts; production systems usually prefer to list the allowed origins explicitly and pair the header with Vary: Origin so caches serve the correct variant to each requesting origin rather than one response to everyone.

Requests that carry credentials, such as images behind cookie-based authentication, need stricter handling: Access-Control-Allow-Origin must echo the specific requesting origin instead of using *, and the response must also include Access-Control-Allow-Credentials: true. Most image GET requests are simple requests that do not trigger a preflight, but requests with custom headers do trigger an OPTIONS preflight, and the server must answer that correctly or the browser blocks the request before it ever completes.

Where you configure the headers depends on how the image service is hosted. A self-hosted Nginx setup adds an add_header directive with the appropriate Access-Control-Allow-Origin value in the location block. Object storage services such as OSS and S3 expose a CORS rules console where the same policy is declared declaratively. Third-party image hosts are the difficult case: some offer CORS configuration, many do not, and for those you must fall back to a proxy or a download-and-convert approach. Confirm this capability during vendor selection rather than discovering it during an outage.

Whichever path you choose, verify the headers on the real response with the browser's developer tools rather than trusting the documentation. Caching layers, CDN rewrites, and redirects can all strip or alter headers, and the response you actually receive is the only thing that matters to the browser's security check.

Code Example: Fetch and Convert to a Data URL

When the image server cannot be configured for CORS at all, the most universal fallback is to fetch the image with the fetch API and convert it into a data URL for embedding in the template. fetch itself is bound by the same-origin policy, but it can carry request headers and handle cross-origin behavior explicitly, and when combined with a proxy or a server-side forwarder it can reach resources that an img tag cannot. Once converted, the data URL is same-origin by construction, and the PDF pipeline reads it with no restrictions whatsoever.

The conversion is three steps: fetch the image bytes, turn the response into a Blob, and read the Blob as base64 with FileReader. The resulting data URL embeds the image directly into the template string, which makes the template self-contained and offline-capable, and it also removes the CORS question permanently. The trade-off is that base64 inflates the payload by about a third, so for batches of large images you should watch both template size and memory usage rather than converting everything indiscriminately.

The code below converts a cross-origin image to a data URL and inserts it into the template: fetch retrieves the Blob, FileReader asynchronously produces the base64 string, and the result is spliced into an img tag with the data:image/png;base64 prefix. Every step uses standard web APIs with no extra dependencies, and the error handling keeps a failed image from silently producing a blank region in the delivered document. This same helper works for fonts and any other binary asset your templates need.

One refinement worth adding in production is a timeout on the fetch: a hanging image server should fail fast and trigger the fallback rather than blocking the whole export. Wrap the fetch in a Promise.race with a timeout, or use AbortController, and the preload step becomes robust against slow or dead third-party services.

import { DomPDF } from 'dompdf.js';

async function toDataUrl(url) {
  const res = await fetch(url);
  const blob = await res.blob();
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = () => resolve(reader.result);
    reader.onerror = reject;
    reader.readAsDataURL(blob);
  });
}

const imgUrl = 'https://cdn.example.com/photos/cover.jpg';
const dataUrl = await toDataUrl(imgUrl);

const html = `
  <style>body { font-family: 'Source Han Sans SC', sans-serif; }</style>
  <h2>Cover Image Example</h2>
  <img src="${dataUrl}" style='width:100%'>`;

const pdf = new DomPDF({ format: 'A4', margin: '20mm' });
pdf.addPage(html, { format: 'A4' });
pdf.save('cors-demo.pdf');

Proxy Solutions vs Same-Origin Hosting

When the image service is completely out of your control — no CORS support, no public access — the robust answer is a same-origin proxy: the backend accepts an image URL, fetches it on behalf of the client, and streams the bytes back. From the browser's perspective the request is same-origin, so the cross-origin problem disappears entirely. A proxy can also layer in authentication, caching, rate limiting, and logging, which makes it a natural single gateway for all third-party image traffic inside an organization.

Same-origin hosting is the more thorough solution: migrate the images from the third party to your own static asset service, where they share the origin with the page and no CORS question exists at all, and where your caching and CDN policies apply uniformly. The cost is the migration effort and the storage bill, so this fits businesses with a manageable, stable image set. For temporary third-party integrations, a proxy or a data URL is the more economical choice, and you should not over-engineer around content that may be gone in a quarter.

The three approaches can coexist: core assets are hosted same-origin, third-party material flows through a proxy, and transient content is converted to data URLs on the fly. The selection criterion is the controllability and usage frequency of the source — images used constantly are worth migrating, one-off content is not. Composed this way, cross-origin problems stop being a bottleneck, and the strategy can evolve naturally as the business changes.

A practical note on proxies: stream the response rather than buffering it entirely in memory, especially for large images or high concurrency. Streaming keeps the memory footprint of the proxy flat, and it preserves the original bytes exactly, which matters for signature-based or integrity-checked image pipelines downstream.

Caching, Credentials, and Common Pitfalls

CORS and caching interact in a subtle and common failure mode: after an image is cached by a CDN or the browser, if the cached response lacks the CORS headers, reads fail even though the origin server is configured correctly. The fix is to make sure both the origin and every cache layer return Access-Control-Allow-Origin, and to set Vary: Origin so different requesting origins get their own cached copies instead of sharing one response that may authorize the wrong party.

Redirects are the second classic pitfall. When an image URL redirects, the browser applies the cross-origin check to the final response after the redirect, so if the redirect target lacks CORS headers the request fails even when the original URL was fine. During diagnosis, inspect the final response headers rather than the first request's; if the redirect target is stable, consider using it directly as the image source to remove the hop entirely.

Mixed content and credential policy round out the list. An HTTPS page that references an HTTP image is blocked by the browser regardless of CORS, so everything must be upgraded to HTTPS. Image requests carrying cookies must be treated as credentialed requests, with the fetch call specifying the credentials mode explicitly. Adding these checks to your troubleshooting checklist means most cross-origin problems are located and resolved in one pass instead of after several blind attempts.

Finally, be aware that some browsers enforce CORS on canvas reads more aggressively in private browsing or with extensions installed. If a failure reproduces in one environment but not another, reproduce it in a clean profile before blaming the server configuration, because environment-specific behavior wastes hours when chased as a server bug.

Cross-Origin Troubleshooting Checklist

When cross-origin images fail to appear in the PDF, work through this order: first open the browser developer tools and inspect the image request's response headers to confirm that Access-Control-Allow-Origin is present and permits the current origin; then check whether the request went through a redirect and whether the final response carries the CORS headers; finally confirm the image actually loaded with a 200 status rather than a 403 or 404. Each step eliminates an entire category of causes.

Once the server side looks correct, move to the frontend: verify the template references the right address, the fetch call uses the necessary mode and credentials, and the converted data URL is complete and usable. Checking the two sides in sequence usually locates the root cause within ten minutes, and it keeps the search confined to one layer rather than bouncing between possibilities. Write down what you find; the same diagnosis will recur with the next third-party image source.

Finally, build prevention into the workflow: route image access through a shared utility with same-origin first, proxy as backup, and data URL as the emergency path; and verify CORS for every new image source before it goes live. Once these practices become team conventions, cross-origin images stop being an incident source, the PDF pipeline stays predictable, and new integrations land without a debugging session attached.

If you manage many third-party sources, consider adding a small automated check that fetches each configured image URL and asserts the CORS headers on the response. A five-minute script running in CI catches header regressions the moment a vendor changes their configuration, which is far cheaper than discovering it through a customer-facing PDF failure.

Run the same check in the job that validates image sources, and the CORS assertion becomes part of onboarding for every new vendor rather than an afterthought.

⚡ 现场演示(点击生成 PDF)

下面的按钮用 dompdf.js 在浏览器端实时生成 PDF,无需后端:

Hello from dompdf.js!

这是由 dompdf.js 渲染的示例 PDF 内容。