skip to content

questions

4

A cross-origin response carries a custom X-Gauge-Reported-At header that script cannot read - which response field makes it readable?

level: juniorimportance: must knowfreq 58%

answer

  1. the script sees a trimmed header list
  2. seven safelisted response-header names
  3. present on the wire, absent in script
  4. granting the body is not granting a header
  5. the server must name the extra field

basics

~10 s

Access-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 s

A 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 lines
http
HTTP/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

for a junior

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.

for a middle

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.

for a senior

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.

for a principal

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
open as a page

Which values may Access-Control-Allow-Origin carry in one response, and why can it never list three allowed hosts?

level: middleimportance: must knowfreq 72%

basics

~20 s

Access-Control-Allow-Origin carries exactly one value: one serialized origin, the literal null, or the wildcard. Its grammar defines no list, so a feed serving several caller origins emits the one value that fits the current request rather than enumerating them.

open as a page

A preflight answer lists only PUT in Access-Control-Allow-Methods; which cross-origin methods may the caller still attempt?

level: middleimportance: should knowfreq 48%

basics

~20 s

GET, HEAD and POST remain available: they are CORS-safelisted methods and pass the check without appearing in the grant. Access-Control-Allow-Methods is a comma-separated list that adds methods beyond those three, so listing PUT removes nothing.

open as a page

A feed answers Access-Control-Allow-Headers with the wildcard, yet a caller sending Authorization still fails the check - why?

level: seniorimportance: should knowfreq 40%

basics

~10 s

Authorization is the CORS non-wildcard request-header name: the wildcard in Access-Control-Allow-Headers covers other unsafe request headers but never that one, so a server must list Authorization by name for a caller that sends it.

open as a page