skip to content

What happens to a `Set-Cookie` on a cross-origin response whose CORS check then fails?

level: middleimportance: should knowfreq 34%

answer

  1. two steps, and only one is filtered
  2. processed while fetching, checked afterwards
  3. the write survives the refused read
  4. credentials mode gates the storing
  5. reload reveals the sign-in that failed

basics

~20 s

If the request's credentials mode was "include", the cookie is already stored: the browser processes Set-Cookie while fetching, before the CORS check runs. The check then refuses script the response, so the write happened and the read did not.

solid answer

~40 s

`Set-Cookie` on a cross-origin response is honoured only when the request's credentials mode is `"include"` — an uncredentialed cross-origin fetch ignores it entirely. When the mode is `"include"`, the cookie is processed as part of receiving the response, which happens *before* the CORS check filters what script gets. So a response that sets a session cookie and then fails the check leaves the cookie stored while script receives a network error: a `TypeError`, `status 0`, no headers, no body. A cross-origin sign-in can therefore report failure to the page and still log the user in, which becomes visible on the next reload. Script can never read `Set-Cookie` itself, even if the server names it in `Access-Control-Expose-Headers`.

code

http · 7 lines
http
HTTP/1.1 200 OK
Content-Type: application/json
Set-Cookie: tenant_session=8f2b9c
Access-Control-Allow-Origin: https://portal.example
Vary: Origin

{"tenant":"t-4471"}

go deeper

for a junior

Remember that a browser can store a cookie from a response your code was never allowed to read; the two outcomes are decided at different moments.

for a middle

Explain the ordering: Set-Cookie is processed while the response is received, and the CORS check runs afterwards on what script may see.

for a senior

Recognise the incident shape — a sign-in that reports failure and then works after a reload — and know it points at a grant missing on one response path.

for a principal

The general lesson is that a read-side control never unwinds a side effect, which is the same property that makes cross-site writes a separate problem from cross-site reads.

## Two things happen to one response When a cross-origin response arrives, the browser does two separable things with it: 1. **Processes it as an HTTP response.** Among other things, that means acting on `Set-Cookie` — storing the cookie in the browser's cookie store. 2. **Filters it for script.** The CORS check runs, and either the response is handed to the calling code or it is replaced by a network error. Those steps happen in that order, and the second cannot undo the first. A response whose CORS check fails has already had its cookies stored. ## The credentials mode gates step one Step one is conditional. `Set-Cookie` on a **cross-origin** response is acted on only when the request's credentials mode is `"include"`: - mode `"omit"` or `"same-origin"` on a cross-origin request → no credentials sent, and `Set-Cookie` on the way back is ignored; - mode `"include"` → credentials sent, and `Set-Cookie` on the way back is honoured. So the credentialed mode is what makes the write possible in the first place. The CORS grant is not: it governs only step two. ## The confusing incident A tenant portal signs in against an authentication API on a second host. The call is credentialed. The API answers `200`, sets the session cookie, and — because the sign-in path's response-header block was never updated — omits `Access-Control-Allow-Credentials`. What the team sees: 1. The page reports a failed sign-in and shows an error, because script received a network error with no status and no body. 2. The API's log shows a successful authentication. 3. The user reloads the portal out of frustration and is **signed in**. Every one of those observations is correct, and together they look impossible until you separate the write from the read. | | Write (`Set-Cookie`) | Read (response body) | |---|---|---| | Gated by | Credentials mode `"include"` | The CORS check on the response | | Happens when | The response is received | After the check passes | | Effect of a failed check | None — already done | Replaced by a network error | | Visible to script | Never | Only when the check passes | ## Script never reads `Set-Cookie` A related and frequently attempted workaround does not work: naming `Set-Cookie` in `Access-Control-Expose-Headers` does not make it readable. The browser withholds that field from script regardless of the grant, on any response, cross-origin or not. Exposure widens which headers script may read among those it is permitted to see at all; it cannot promote a field that is never handed to script. This matters for reasoning about the incident above: there is no way for the page to observe that the cookie was set. The only evidence is the effect on the next request. ## Why the order is designed this way CORS is a rule about **reading**, applied at the boundary between the network stack and the calling script. Cookie storage sits below that boundary, in the part of the browser that handles HTTP. Making a failed check retroactively unwind cookie storage would mean pushing a script-facing permission rule down into the HTTP layer, which is not where it lives. The consequence — a side effect that survives a refused read — is the same consequence the same-origin policy has always had: it never prevented the request or its effects, only the reading of the answer. ## What to carry away - `Set-Cookie` on a cross-origin response is honoured only under credentials mode `"include"`. - The cookie is stored before the CORS check runs, so a failed check does not unset it. - Script gets a network error: `TypeError`, `status 0`, no headers, no body. - A cross-origin sign-in can report failure and still succeed; the reload reveals it. - `Set-Cookie` is never readable by script, exposed or not.

  • The same response is fetched with credentials mode `"same-origin"` instead. What changes?
    Nothing is stored. On a cross-origin request that mode attaches no credentials and the response's `Set-Cookie` is ignored, so the cookie never enters the store whatever the grant says. The read side is unaffected by the change in isolation: script still needs a passing CORS check, but the credentials rules no longer apply to it, so the response need not carry `Access-Control-Allow-Credentials` at all.
  • Can the page detect that the cookie was set, given the call reported an error?
    Not from the response. `Set-Cookie` is never handed to script, and naming it in `Access-Control-Expose-Headers` does not change that; a failed check leaves no headers to read in any case. The only observable evidence is behavioural — the next request carries the cookie and the server treats the user as signed in.

saying these in an interview costs you the question

  • A failed CORS check discards the response, so no cookie is stored
  • Naming Set-Cookie in Expose-Headers lets script read it
  • Set-Cookie works cross-origin regardless of the credentials mode
  • The browser rolls back side effects when it blocks a read
  • Script receiving an error means the server rejected the sign-in