Under credentialed mode, what does `Access-Control-Expose-Headers: *` make readable to the calling script?
answer
- the asterisk stops being special
- matched as a literal name
- only for uncredentialed requests does it expand
- seven safelisted response names remain
- no error, just a missing value
basics
~20 sOnly a response header whose name is literally *. Once the request's credentials mode is "include", the wildcard in the grant fields stops being a wildcard and is matched as an ordinary field name, so nothing extra is exposed.
solid answer
~30 sThe `*` in `Access-Control-Expose-Headers`, `Access-Control-Allow-Methods` and `Access-Control-Allow-Headers` is expanded into "everything" **only** for requests that are not credentialed. When the credentials mode is `"include"`, no expansion happens and the token is compared as a literal name — so `Access-Control-Expose-Headers: *` exposes a header called `*`, which no server sends. Script is left with the CORS-safelisted response-header names it could already read: `Cache-Control`, `Content-Language`, `Content-Length`, `Content-Type`, `Expires`, `Last-Modified` and `Pragma`. The same literal reading applies in a preflight answer, so a credentialed request using a method or header covered only by a `*` fails. Under credentials every name must be spelled out.
code
http · 7 linesHTTP/1.1 200 OK
Content-Type: application/json
X-Total-Count: 47
Access-Control-Allow-Origin: https://portal.example
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: *
Vary: Origingo deeper
Know that script cannot read arbitrary response headers by default, and that the server names the extra ones it is willing to expose.
Explain that the wildcard in the exposure and preflight grants is expanded only for uncredentialed requests, and name the seven headers readable without any grant.
Recognise the silent symptom — one missing value, no error anywhere — and know it appears the moment a working feature starts carrying the session.
The point to generalise is that the protocol forces credentialed grants to be explicit, which is what makes a header block reviewable rather than inherited.
## The rule Three CORS grant fields accept `*` as a wildcard: - `Access-Control-Expose-Headers` — which response headers script may read; - `Access-Control-Allow-Methods` — which methods a preflight clears; - `Access-Control-Allow-Headers` — which request header names a preflight clears. In each case the expansion of `*` into "all of them" is conditional: it happens only when the request's credentials mode is **not** `"include"`. Under credentials the token is left alone and matched as what it literally is — a field name, or a method name, consisting of one asterisk. Nothing errors. There is no warning. The grant is simply satisfied by nothing, because no response carries a header named `*` and no request uses a method named `*`. (A fourth field, `Access-Control-Allow-Origin`, also takes `*`, and its behaviour under credentials is a flat refusal rather than a literal reading — the response fails the check outright.) ## What script is left with Without any exposure grant, script can read a fixed set of response header names — the **CORS-safelisted response-header names**: - `Cache-Control` - `Content-Language` - `Content-Length` - `Content-Type` - `Expires` - `Last-Modified` - `Pragma` That is the floor, and a credentialed response granting `*` sits exactly on it. Everything else the server sent is present in the browser and withheld from script. ## The failure this produces A tenant portal pages through maintenance requests and reads a total count from a custom response header. The API's header block carries `Access-Control-Expose-Headers: *`, and during development, before the calls carried the session cookie, it worked perfectly. The day the calls became credentialed, the count became undefined — and every other part of the feature kept working, because the body was still readable. That is the shape to recognise: 1. The response arrives intact; the CORS check passes, since `Allow-Origin` and `Allow-Credentials` are correct. 2. The body reads normally. 3. One custom header is missing from what script can see, and only that. 4. Nothing anywhere reports an error, because nothing failed — a grant was satisfied by nothing. | Field | Credentials mode not "include" | Credentials mode "include" | |---|---|---| | `Access-Control-Expose-Headers: *` | Exposes every header script is allowed to see | Exposes a header named `*` — in effect, nothing | | `Access-Control-Allow-Methods: *` | Clears any method | Clears a method named `*` | | `Access-Control-Allow-Headers: *` | Clears any request header name | Clears a header name that is one asterisk | | `Access-Control-Allow-Origin: *` | Grants any origin | Refused: the check fails outright | ## Why the protocol does this A wildcard is a statement that the resource does not care who reads it. That is a coherent position for a response computed without reference to any user — and an incoherent one for a response computed *for* a signed-in tenant. Rather than add a second syntax for "wildcard, but only for anonymous reads", the protocol makes the existing wildcard inert the moment credentials are in play. The effect is that every credentialed grant is **explicit**: an operator reading the configuration can see exactly which origins, methods and header names were intended. That also means a copied-from-anywhere header block is likely to have been written under the uncredentialed reading, and will quietly narrow the moment the first cookie is attached. ## Getting it right 1. Under credentials, enumerate: list the response header names script actually needs in `Access-Control-Expose-Headers`, and the methods and request header names the preflight must clear. 2. Treat a `*` in a credentialed grant as a defect in review, not as a shorthand — it is indistinguishable from a typo and behaves like one. 3. Remember that a header the browser holds but does not expose is not a server problem: the bytes arrived, and the server has nothing further to do except name them. 4. Expect no error signal. The symptom is a missing value, not a failure. ## What to carry away - `*` is a wildcard in these three fields only for uncredentialed requests. - Under credentials it is a literal name, so the grant matches nothing. - The seven safelisted response-header names remain readable regardless. - `Access-Control-Allow-Origin: *` is a different story: refused, not read literally.
- The same response is fetched without credentials and the custom header becomes readable. Why?Because the wildcard is expanded only when the credentials mode is not `"include"`. Uncredentialed, `Access-Control-Expose-Headers: *` means every header script may see; credentialed, it means a header literally named `*`. The response bytes are identical in both cases — what changed is how the browser reads the grant, which is why the bug appears at the moment a feature starts carrying the session.
- Is `Access-Control-Allow-Origin: *` also read literally under credentials?No, and the distinction is worth keeping straight. That field is not matched literally; a credentialed request whose response grants `*` fails the CORS check outright, and script gets a network error. The other three fields fail silently by matching nothing, which is why they are far harder to notice.
saying these in an interview costs you the question
- The wildcard exposes every header regardless of credentials
- A credentialed wildcard grant raises an error the browser reports
- Only Allow-Origin changes meaning when credentials are involved
- The server must resend the header for script to read it
- Exposing headers is about the server sending them, not script reading them