skip to content

A tenant portal's credentialed cross-origin call returns `200` with an anonymous body though both legs grant credentials — why?

level: seniorimportance: must knowfreq 55%

answer

  1. three conditions, only one is CORS
  2. read permission, not proof of identity
  3. nothing errored anywhere
  4. inspect the request, not the response
  5. the Cookie header settles it

basics

~20 s

Because no credential was attached to the request. The CORS grant governs whether script may read the response; it says nothing about whether the cookie travelled, so the API answered an anonymous request correctly and nothing failed.

solid answer

~40 s

A correct grant on both legs proves exactly one thing: the browser will hand the response to script. It is not evidence that the request carried the tenant's session. If no `Cookie` header arrived, the API authenticated nobody and returned the anonymous view — an empty list, a `200`, and no error anywhere in the system. The diagnosis is one step: look at the **request** the server received, not the response headers it sent. Whether the browser attaches a cookie to a cross-site request is decided by the cookie's own attributes and by the request's credentials mode, both of which sit outside CORS entirely. This is the failure mode CORS is worst at signalling, because from the protocol's point of view everything worked.

code

http · 12 lines
http
GET /maintenance-requests HTTP/1.1
Host: api.portal.example
Origin: https://portal.example
Accept: application/json

HTTP/1.1 200 OK
Content-Type: application/json
Access-Control-Allow-Origin: https://portal.example
Access-Control-Allow-Credentials: true
Vary: Origin

{"requests":[]}

go deeper

for a junior

Understand that a cross-origin call can succeed completely while the server treats the caller as anonymous, because the two things are decided separately.

for a middle

Separate the three conditions — credential attached, server authenticates, script may read — and say which one the CORS grant covers.

for a senior

Show the diagnosis order: inspect the request the server received first, because the Cookie header decides in one step which half of the system to open.

for a principal

The judgment worth voicing is that this failure emits no signal at all, so it argues for logging credential presence on cross-origin paths rather than trusting a green check.

## Three independent conditions For a cross-origin call to show a tenant their own maintenance requests, three separate things must hold, and each is governed by a different mechanism: 1. **The credential is attached.** The calling code opted into credentials mode `"include"`, and the browser's own cookie rules agreed to send that cookie on a cross-site request. 2. **The server recognises it.** The API reads the session, authenticates the tenant, and produces the personalised body. 3. **Script may read the answer.** The CORS check passes on both legs: the exact origin echoed, `Access-Control-Allow-Credentials: true` present. A green CORS check is evidence for **condition 3 only**. It is silent about the first two, and the reported symptom — a `200` with an anonymous body — is what condition 1 failing looks like from the browser. ## Why nothing reports an error Every participant behaved correctly: - The browser sent a cross-origin request it was entitled to send, without a credential. - The API saw no session, applied its normal rule for anonymous callers, and answered `200` with an empty list. That is not an error; it is the right answer to the question it was asked. - The CORS check passed, so the browser handed script the response. - Script received a valid response with an empty list and rendered an empty page. There is no failed status, no network error and no console message, because nothing failed. This is the most expensive shape of CORS defect precisely because it produces no signal — teams lose hours re-reading response headers that are already correct. ## The one-step diagnosis Look at the request the server received, not the response it sent. - A `Cookie` header present on the request → the credential arrived, and the problem is in step 2: session lookup, session scope, or the handler's authorization logic. - A `Cookie` header absent → the credential never arrived, and nothing on the response side can fix it. Everything you would change there is already right. That single field settles which half of the system to investigate, and it is visible in the server's own request log without any client-side tooling. | Observation | What it rules out | Where to look next | |---|---|---| | `200`, anonymous body, no errors | A CORS grant problem | Whether a credential arrived | | `Cookie` absent on the request | Server-side session handling | Credentials mode and the cookie's own attributes | | `Cookie` present, body still anonymous | Credential attachment | Session lookup and authorization | | Network error, `status 0`, no body | An authentication problem | The grant on whichever leg failed | ## Why CORS cannot help you here CORS is a **read** permission. Its entire vocabulary concerns whether script at one origin may see a response produced for another. It has no field that says "a credential was attached", no field that says "the server authenticated somebody", and no way to distinguish a personalised body from an anonymous one — the body is opaque to the check. Whether the browser attaches the cookie at all is a decision made by the cookie's own attributes together with the request's credentials mode, before CORS is consulted. That is a different specification's subject, and the important thing to hold here is the *consequence*: a grant is not evidence of attachment, and the two are configured independently. A team can get the grant perfect and still never send a cookie, or send the cookie faithfully and still have script refused the answer. ## What a good answer demonstrates - Naming the three conditions separately rather than collapsing them into "CORS works". - Reaching for the **request** as the first artefact, not the response. - Knowing that an empty result with `200` is a server answering honestly, not a browser interfering. - Stating the limit of the grant out loud: it governs reading, never sending, never processing, and never the shape of the body. ## What to carry away - Both legs granting credentials proves only that script may read the response. - A `200` with an anonymous body is the signature of a credential that never arrived. - The `Cookie` header in the server's request log is the one-step discriminator. - Attachment and grant are configured in different places and fail independently.

  • The `Cookie` header is present on the request but the body is still anonymous. Where do you look?
    At the server. The credential arrived, so attachment and CORS are both fine; the problem is in session lookup or authorization — the session may be expired, scoped to a different path or host name than the one serving the API, or the handler may be reading a different cookie name than the one that was set. None of it is a CORS question any more.
  • Why is a network error with no status a better signal than the `200` in this scenario?
    Because it tells you the grant failed, which is a small, well-defined search space: the origin echoed, the credentials field, and which leg produced it. The `200` with an anonymous body tells you nothing failed, which is far less informative — it is consistent with a missing cookie, an expired session, or a genuinely empty result, and only the request log separates them.

A signed permission slip lets you open the envelope. It does not mean anyone put a letter inside — and an empty envelope opens just as smoothly as a full one.

saying these in an interview costs you the question

  • A correct CORS grant proves the session cookie was sent
  • The empty body means the browser stripped the data
  • The API must be misconfigured, since the call returned nothing useful
  • Adding more grant headers to the response will fix it
  • CORS would have reported an error if the credential were missing
  • A 200 rules out any credential problem