Access-Control-Expose-Headers: why response.headers.get() returns null (tested)

On a cross-origin response the server can send any header, but your JavaScript only sees a short safe list unless the server exposes more. We read every header in Chrome with and without Access-Control-Expose-Headers.

1. What the page could read

Header on the responseNo expose headerContent-Disposition, X-Total-Count*
Content-Type, Cache-Control, Content-Languagevisiblevisiblevisible
Content-Dispositionnullvisiblevisible
X-Total-Count (custom)nullvisiblevisible
Retry-Afternullnull (not listed)visible

Without the header, [...response.headers.keys()] listed only cache-control, content-language, content-type. DevTools' Network tab still shows every header, which is why this looks like a browser bug: the header arrived, the page just isn't allowed to read it.

2. The fix

Access-Control-Expose-Headers: Content-Disposition, Retry-After, X-Total-Count

# nginx
add_header Access-Control-Expose-Headers "Content-Disposition, Retry-After, X-Total-Count" always;

Typical cases: reading the download file name from Content-Disposition, honouring Retry-After on a 429, pagination totals (X-Total-Count, Link), and rate-limit headers. * exposed everything in our test, but per the Fetch spec it is treated as a literal header name on requests with credentials (cookies), so list names explicitly there.

3. Expose-Headers vs Allow-Headers

Frequently asked questions

Why does response.headers.get() return null in fetch?

The response is cross-origin and the header isn't exposed. In our Chrome test Content-Disposition, Retry-After and X-Total-Count were null until listed in Access-Control-Expose-Headers.

Which response headers can JavaScript read without exposing them?

The CORS-safelisted ones. Our test saw Cache-Control, Content-Language and Content-Type by default (the spec also lists Content-Length, Expires, Last-Modified and Pragma).

Does Access-Control-Expose-Headers: * work?

For requests without credentials, yes: it exposed every header in our test. With cookies, the spec treats * literally, so list the names.

What's the difference between Access-Control-Allow-Headers and Expose-Headers?

Allow-Headers is for request headers the page sends; Expose-Headers is for response headers the page reads.

Written with AI assistance. Measured on 2026-10-01 in Chromium 153 (full build): a page on localhost:8994 fetched a cross-origin URL whose response carried Content-Disposition, Retry-After, X-Total-Count, Cache-Control and Content-Language, with three Access-Control-Expose-Headers values; we read response.headers in the page. The rule that '*' doesn't work for credentialed requests is from the Fetch spec, not this test; so are the DevTools remark and the nginx line. PlainHeaders is our extension; the other methods are free.