A cross-origin response carries a custom X-Gauge-Reported-At header that script cannot read - which response field makes it readable?
answer
- the script sees a trimmed header list
- seven safelisted response-header names
- present on the wire, absent in script
- granting the body is not granting a header
- the server must name the extra field
basics
~10 sAccess-Control-Expose-Headers, sent by the server on that response, names the extra fields script may read. Without it a cross-origin caller sees only seven names: Cache-Control, Content-Language, Content-Length, Content-Type, Expires, Last-Modified and Pragma.
solid answer
~40 sA cross-origin response arrives at the browser whole and reaches the calling script trimmed. Once the CORS check passes, script gets a filtered view whose header list is cut down to the seven **CORS-safelisted response-header names** - `Cache-Control`, `Content-Language`, `Content-Length`, `Content-Type`, `Expires`, `Last-Modified` and `Pragma`. Everything else the server sent is still on the wire and still visible to anything that is not a browser, but it is simply absent from the object script holds, with no error and no warning. The server widens that list per response with `Access-Control-Expose-Headers = #field-name`, a comma-separated list of field names: `Access-Control-Expose-Headers: X-Gauge-Reported-At`. Granting the response and granting one header of it are two separate grants, and `Access-Control-Allow-Origin` only does the first.
code
http · 8 linesHTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: max-age=60
X-Gauge-Reported-At: 2026-09-19T06:12:00Z
Access-Control-Allow-Origin: https://charts.example.net
Access-Control-Expose-Headers: X-Gauge-Reported-At
{"station":42,"level_m":3.18}go deeper
Recall the two-grant split: one field grants the response, another grants a header of it. Knowing that a custom header needs naming before script can read it is enough at this stage.
Explain the mechanics: the browser builds a filtered response, the seven safelisted response-header names survive by default, and a comma-separated grant adds more for that one response.
Show that you diagnose it by the silence: no error, a readable body, one absent field. Then check where the grant is set, because a server that sets it only on its preflight branch leaves the real response filtered.
Frame it as an interface decision. A value callers must act on is safer in the body than in a header that every response must remember to expose, and a feed with many consumers should decide that once.
A cross-origin response reaches the browser whole and reaches the calling script trimmed. Which headers survive that trim is decided by one response field, and a feed whose value is in a custom header meets the rule immediately. ## The seven names script reads by default When the CORS check on a cross-origin response succeeds, the browser does not hand script the response it received on the wire. It hands over a **CORS filtered response**: the status is visible, the body is readable, and the header list is reduced to a fixed set of names, the **CORS-safelisted response-header names**: - `Cache-Control` - `Content-Language` - `Content-Length` - `Content-Type` - `Expires` - `Last-Modified` - `Pragma` That list is fixed by the specification. It is not a server setting and it does not grow because a response happens to carry more headers. A river-gauge feed that publishes readings as JSON and stamps each response with `X-Gauge-Reported-At` so a charting page can say how stale the reading is has put the one value the caller most needs outside that list. ## What the caller actually observes Nothing fails. The CORS check passed, the status is readable, the body parses, and only the header is missing - reading it yields nothing at all. That silence is why the failure is nearly always misdiagnosed as *the server did not send it*, or as an intermediary stripping it. The header is on the wire: anything that is not a browser - another service, a scheduled job, a client written against the same feed - reads it without any grant, because the filtering is the browser's behaviour, not the server's. ## The field that widens the list `Access-Control-Expose-Headers = #field-name` is a **response** header the server sends. The `#` is HTTP's comma-separated list construct, so the field names one or more field names, and each named field is added to the seven for that response: ```http HTTP/1.1 200 OK Content-Type: application/json X-Gauge-Reported-At: 2026-09-19T06:12:00Z Access-Control-Allow-Origin: https://charts.example.net Access-Control-Expose-Headers: X-Gauge-Reported-At ``` Field-name matching is case-insensitive, as it is everywhere in HTTP, so the casing in the grant need not match the casing on the header. Naming a field that the response does not carry is harmless: exposure only ever widens what may be read, it never creates a header. ## Two grants, and they are not the same grant | grant | field | what it decides | |---|---|---| | may this caller read the response at all | `Access-Control-Allow-Origin` | whether script receives a body and a status, or a network error | | may this caller read *this header* of it | `Access-Control-Expose-Headers` | whether one named field appears in the filtered header list | That asymmetry is the whole shape of the subject. A caller can hold a response whose body it parses happily and still be blind to a header sitting three lines above that body, because permission to read a response and permission to read a field of it are granted separately and by different fields. ## The wildcard form For a request made **without** credentials, `Access-Control-Expose-Headers: *` exposes every response header name rather than a named few. Because `*` is read as a wildcard there, there is no way to expose a field whose name is literally `*`. What credentialed mode does to this and every other grant field is a separate subject; write the non-credentialed reading and check the credentialed one before relying on a wildcard. ## What exposure does not do 1. **It does not make the header exist.** If the response does not carry the field, naming it changes nothing. 2. **It does not affect same-origin reads.** A same-origin response is handed to script with its full header list, so the field is inert there - which is exactly why a header that works locally disappears the moment the page is served from a second host. 3. **It does not affect non-browser clients.** They never applied the filter in the first place. 4. **It is decided on the response script reads.** The preflight answer grants methods and request-header names for the request that follows; exposure is stated on that request's own response. The practical rule for a feed: every value a caller is expected to act on either belongs in the body, where no grant is needed, or in a header that the response names in `Access-Control-Expose-Headers`.
- Does Access-Control-Expose-Headers change anything for a same-origin caller?No. The filtered view is built only for a cross-origin response; a same-origin response is handed to script with its whole header list. The field is inert there, which is why a header read fine from the same host and vanishes when the page moves to a second host.
- Should the exposure grant go on the preflight answer or on the actual response?On the actual response - the one script will read. A preflight answer grants a method and request-header names for the request that follows it; it says nothing about which of that request's response headers become readable. A server that sets exposure only on its preflight branch leaves the real response filtered.
A reading room hands you the document but not the archivist's note clipped to it; the note travelled with the file the whole way, and is withheld unless the depositor said it may be shown.
saying these in an interview costs you the question
- Says the server never sent the header, so the backend is at fault
- Thinks Access-Control-Allow-Origin alone makes every response header readable
- Believes the missing header means the CORS check failed
- Assumes the seven safelisted response-header names are server-configurable
- Thinks the server strips the header, rather than the browser filtering it