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.