Skip to content
IIIF & Image Interoperability

A IIIF CORS error means your image server returned the file correctly, but it forgot to add a header telling the browser that another website is allowed to read it. The fix is almost always one line: send an Access-Control-Allow-Origin: * header on every response from your IIIF service. The image is not missing — the browser is just being protective.

What is CORS, really? ​

CORS stands for Cross-Origin Resource Sharing. By default a browser will not let JavaScript on viewer.example.com read data fetched from images.example.org unless the server at images.example.org explicitly says "anyone may read this". That permission is a response header. A IIIF viewer is JavaScript on one domain pulling tiles and info.json from another, so it runs straight into this rule.

You will see something like this in the browser console:

Access to fetch at 'https://images.example.org/iiif/3/map_0042/info.json'
from origin 'https://viewer.example.com' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.

Why does the URL work in my address bar but not in the viewer? ​

Because typing a URL into the address bar is a top-level navigation, which CORS does not restrict. A viewer makes a cross-origin fetch from inside a page, which CORS does restrict. That is why "but it loads fine when I click the link!" is the most common false reassurance. Test the way the viewer does:

bash
curl -I -H "Origin: https://viewer.example.com" \
  https://images.example.org/iiif/3/map_0042/info.json

Look for Access-Control-Allow-Origin in the response. If it is absent, that is your whole problem.

How do I add the header? ​

The exact place depends on what sits in front of your image server. Here are the common fixes:

nginx
# nginx, on the IIIF location block
location /iiif/ {
    add_header Access-Control-Allow-Origin "*" always;
}
apache
# Apache, requires mod_headers
<Location "/iiif/">
    Header set Access-Control-Allow-Origin "*"
</Location>
yaml
# Cantaloupe.properties
endpoint.public.cors.enabled = true

Cantaloupe ships with a CORS toggle; turning it on adds the header for you. For static IIIF on cloud storage, set the bucket's CORS policy to allow GET from *.

Which responses need the header? ​

Both info.json and the image tiles. A viewer fetches info.json first, then dozens of tile URLs. If only one carries the header you get a confusing partial failure. The safest rule:

ResponseNeeds CORS header?
info.jsonYes
Image tile (.../default.jpg)Yes
The manifest (Presentation API)Yes
Redirects to the aboveYes

Apply the header to the whole service path, not a single endpoint.

What about preflight requests? ​

For ordinary image GET requests, browsers skip the extra check. But if a request carries a custom header, the browser first sends an OPTIONS "preflight" asking permission. Your server must answer that OPTIONS with the allow headers:

nginx
location /iiif/ {
    add_header Access-Control-Allow-Origin "*" always;
    if ($request_method = OPTIONS) {
        add_header Access-Control-Allow-Methods "GET, OPTIONS" always;
        add_header Access-Control-Allow-Headers "*" always;
        return 204;
    }
}

Most IIIF traffic never needs preflight, so do not over-engineer this until you actually see an OPTIONS request failing.

A worked example from a standing start ​

  1. Open the viewer; see the console error naming a missing Access-Control-Allow-Origin.
  2. Run the curl -I -H "Origin: ..." test above and confirm the header is absent.
  3. Add Access-Control-Allow-Origin: * to the IIIF location (or flip Cantaloupe's CORS flag).
  4. Reload your server config (nginx -s reload).
  5. Re-run the curl test; confirm the header now appears.
  6. Hard-refresh the viewer — tiles load.

Key Takeaways ​

  • A CORS error means a missing permission header, not a missing file.
  • The fix is usually Access-Control-Allow-Origin: * on every IIIF response.
  • A URL working in the address bar does not prove CORS is configured.
  • Both info.json and the image tiles must carry the header.
  • Cantaloupe has a built-in CORS switch; nginx/Apache need an add_header rule.
  • Preflight (OPTIONS) only matters for non-simple requests; handle it only if you see it fail.

Frequently Asked Questions ​

What is a CORS error in plain language? ​

It means a web page on one domain tried to read data from your IIIF server on another domain, and the server did not include a header granting permission. The browser blocks the read to protect users, even though the file exists.

Which header fixes most IIIF CORS errors? ​

Access-Control-Allow-Origin. Setting it to an asterisk allows any site to load your public IIIF resources, which is the normal configuration for open cultural-heritage content.

Does the error mean my image is missing? ​

No. CORS errors usually appear alongside a perfectly valid 200 response. The resource is there; the browser simply refuses to let the calling page read it because the permission header is absent.

Why does the image load directly but fail in a viewer? ​

Opening the URL directly is a same-origin or top-level navigation, which CORS does not police. A viewer on a different domain makes a cross-origin fetch, which does, so only the viewer trips the missing header.

Do info.json and the image tiles both need CORS headers? ​

Yes. The viewer fetches info.json and the image tiles as cross-origin requests, so the Access-Control-Allow-Origin header must be present on both, ideally on every response from the image service path.

What is a preflight request and when does it matter? ​

For simple GET image requests browsers skip preflight. It only appears for requests with custom headers or non-simple methods, sending an OPTIONS call first; your server must answer that OPTIONS with the allow headers too.