skip to content

A page calls fetch() against its API and the session cookie is not attached to the request, even though the same cookie is visible in devtools. What does the credentials option in the fetch init control, and what is its default?

level: middleimportance: should knowfreq 55%

answer

  1. ambient credentials, not your own headers
  2. the URL's origin decides by default
  3. cross-origin needs an explicit opt-in
  4. two-sided: client flag and server allowance
  5. the cookie's own attributes still rule

basics

~20 s

credentials controls whether the browser attaches cookies, HTTP authentication entries and TLS client certificates to a fetch request, and whether it stores Set-Cookie from the response. It defaults to 'same-origin', so cross-origin calls need credentials: 'include'.

solid answer

~50 s

`credentials` takes `'omit'`, `'same-origin'` or `'include'`, and it governs the browser's *ambient* credentials — cookies, HTTP authentication entries and TLS client certificates — not headers you set yourself, which are always sent. The default is `'same-origin'`: cookies ride along when the request URL is same-origin with the page, and are dropped when it is not. So a call to an API on another origin sends no cookie at all until you pass `credentials: 'include'`. The same switch governs the response direction — a `Set-Cookie` on a cross-origin response is ignored unless the request was credentialed. Two things `'include'` does not do: it cannot override a cookie's own `SameSite` attribute, and cross-origin it requires the server to opt in as well. Since 2017 the default has been `'same-origin'`; older tutorials that tell you to pass it explicitly are describing the original `'omit'` default.

go deeper

for a junior

Know the three values and the default: credentials decides whether cookies ride along, it defaults to 'same-origin', and a cross-origin API call needs 'include'.

for a middle

Explain that the option governs ambient credentials — cookies, HTTP auth entries, client certificates — but not headers you set, and that it works in both directions, gating whether Set-Cookie on the response is honoured.

for a senior

Debug the whole chain from the request headers outward: same-origin or not, the fetch flag, the cookie's own attributes, the server's opt-in. Explain why a cookie can be present in storage and absent from the request, and fix the right layer.

for a principal

Own the session-transport decision itself: cookie sessions versus token headers, what that implies for cross-origin deployment, CSRF exposure and CDN caching, and whether splitting the app and API across origins is worth the credential complexity it creates.

## What "credentials" means here The word is narrower than it sounds. In the Fetch Standard, credentials are the things the *browser* attaches on your behalf without the script naming them: - cookies matching the request URL - HTTP authentication entries the browser has cached for that origin - TLS client certificates An `Authorization: Bearer …` header you put in `headers` yourself is **not** a credential in this sense. It is an ordinary header, and it is sent regardless of what `credentials` says. This distinction explains a common confusion: a token-based API works fine with the defaults while a cookie-session API mysteriously sees an anonymous user. ## The three values **`'omit'`** — never attach anything, and never store a `Set-Cookie` from the response. Useful for a genuinely anonymous request to a third party where you would rather not leak session state, and for the case where an unexpected cookie would confuse the server's caching. **`'same-origin'`** (the default) — attach when the request URL's origin matches the origin of the page making the call, otherwise behave like `'omit'`. "Origin" means the scheme, host and port triple, so `https://app.example.com` and `https://api.example.com` are different origins, and so are `http://` and `https://` versions of the same host. **`'include'`** — always attach, cross-origin included, and honour `Set-Cookie` on the response. ## The default changed, and the internet remembers the old one When `fetch()` first shipped, the default was `'omit'`: fetch sent no cookies at all unless you asked, which surprised everyone migrating from `XMLHttpRequest`. The specification changed the default to `'same-origin'` in 2017 and browsers followed. That is why so much sample code passes `credentials: 'same-origin'` explicitly. On current browsers it is a no-op — harmless, but a good clue that the surrounding advice is old. ## Cross-origin is a two-sided agreement Setting `'include'` is only your half. For a cross-origin credentialed request the server must also opt in, and the browser will discard an otherwise successful response if it does not — a wildcard `Access-Control-Allow-Origin: *` is specifically not accepted for credentialed requests. The browser-enforcement rules are the CORS story; the point for the fetch API is simply that `'include'` alone is never sufficient, and a request that looks correct in your code can still be rejected on the response side. ## What 'include' cannot do It cannot overrule the cookie itself. A cookie stored with `SameSite=Lax` or `SameSite=Strict` is not sent on a cross-site request no matter what the fetch init says, because that decision belongs to the cookie's own attributes and is made before the fetch option is even consulted. Cross-site cookies therefore need `SameSite=None; Secure` *on the server side* **and** `credentials: 'include'` on the client side. Getting one of the two right and not the other produces exactly the symptom in the question: the cookie is plainly visible in devtools storage, and plainly absent from the request. ## The response direction The flag is symmetric. Under `'omit'`, or cross-origin without `'include'`, a `Set-Cookie` header on the response is ignored — the browser will not store it. This is the usual cause of "login returns 200 but no session cookie appears": the login request itself was cross-origin and uncredentialed. ## Diagnosing it Open the network panel and look at the *request* headers of the failing call. If there is no `Cookie` header, the browser never attached one, and the server is behaving correctly by treating you as anonymous. From there the checklist is short: is the request cross-origin? is `credentials: 'include'` set? does the cookie carry `SameSite=None; Secure`? did the response allow credentials? Chrome's network panel also flags cookies it blocked and why, which usually names the culprit outright. ## A design note The reason cookies are not simply sent everywhere is that they are *ambient* authority: attached automatically, with no code in the page choosing to do so. That is exactly the property that makes cross-site request forgery possible, so every layer of the platform — the default value here, the cookie's `SameSite` attribute, the server's opt-in — narrows when ambient authority travels. Reading `credentials` as one lever among several rather than the single switch is what keeps the debugging sane.

  • Does credentials: 'omit' stop an Authorization header you set yourself from being sent?
    No. The option governs only credentials the browser attaches ambiently — cookies, cached HTTP authentication entries, TLS client certificates. A header you place in the `headers` init is ordinary request data and is sent whatever the value of `credentials`. This is why bearer-token APIs work unchanged on the defaults while cookie-session APIs do not.
  • Why is a cookie visible in devtools still missing from a cross-origin fetch even with credentials: 'include'?
    Because the cookie's own `SameSite` attribute is consulted first. A cookie stored as `SameSite=Lax` or `Strict` is not sent cross-site regardless of the fetch init. Cross-site delivery needs `SameSite=None; Secure` set by the server when the cookie was created, *and* `credentials: 'include'` on the request.
  • A cross-origin login returns 200 but no session cookie is stored. What is the likely cause?
    The request was not credentialed, so the browser ignored the response's `Set-Cookie`. The flag works in both directions: without `'include'` on a cross-origin request the browser neither sends cookies nor stores them. Add `'include'`, and confirm the server permits credentialed responses rather than answering with a wildcard origin.

saying these in an interview costs you the question

  • Says fetch always sends cookies like XMLHttpRequest did
  • Thinks credentials controls the Authorization header too
  • Believes credentials: 'include' overrides SameSite
  • Assumes the client flag alone is enough cross-origin
  • Still passes credentials: 'same-origin' believing it is required

context