Calls from a warehouse wall console to its picking API broke overnight - how do you reproduce and judge the preflight outside the browser?
answer
- the error object tells you nothing
- two exchanges, two judgements
- rebuild the OPTIONS by hand
- send the page's exact Origin value
- ok status AND a passing check
basics
~20 sRe-issue the OPTIONS by hand to the exact URL the page calls, carrying the page's exact Origin and an Access-Control-Request-Method naming the real method, then judge the answer against two conditions at once: an ok status, and a CORS check that passes.
solid answer
~40 sThe error object is not evidence, so the investigation moves to the wire. Send the `OPTIONS` yourself from a client that is not a browser, to the **exact URL** the page calls, with the page's **exact** `Origin` value, `Access-Control-Request-Method` naming the real method, `Access-Control-Request-Headers` naming any header the real call carries, and **no body**. Then judge the answer the way a browser does - against **both** conditions: an **ok status** (200-299) **and** a passing `CORS check`, meaning `Access-Control-Allow-Origin` naming the origin you sent. A `200` with no grant fails; a perfect grant on a `401` or a `3xx` fails. The commonest self-inflicted error is omitting `Origin`, because a server that only emits the grant when one is present will then look perfectly healthy.
code
http · 9 linesOPTIONS /picks/4821/confirm HTTP/1.1
Host: api.example.net
Origin: https://console.example.net
Access-Control-Request-Method: POST
Access-Control-Request-Headers: authorization
HTTP/1.1 200 OK
Allow: GET, POST, OPTIONS
Content-Length: 0go deeper
Know that this failure is diagnosed on the wire, not in the calling code: the OPTIONS request and its answer can be re-sent by hand and read directly.
Be able to say what a hand-built preflight must carry - the exact URL, the exact Origin, the request method being announced, and no body - and what two conditions its answer has to meet.
Demonstrate the traps: omitting Origin clears a broken server, a green status alone proves nothing, and a passing preflight still leaves the real request's own response to be judged.
The lever is that this failure is invisible from the calling side by design, so the organisation needs the grant to be an observable, owned property of each service rather than something rediscovered on an incident call.
## Why the investigation has to leave the browser A blocked cross-origin call rejects with a bare `TypeError`: no status, no headers, no body, and no field naming which condition failed. It cannot tell you whether the preflight failed or the real request did, whether the answer was `200` or `401`, or whether a grant was present and wrong. So there is nothing to read on the calling side, and the only evidence is the **two wire exchanges** - the preflight, and the real request it authorises - plus the server's own log. That is the whole discipline of this leaf: **reconstruct the exchange, judge it the way the enforcing side does, and stop inferring from the error.** ## Rebuilding the preflight by hand From any client that is not a browser, send the `OPTIONS` the browser would have sent. It has to match in five respects: 1. **the same URL** - scheme, host, port and path exactly as the page calls it, since a preflight is answered per URL and a canonical variant may behave differently; 2. **`Origin` set to the page's exact serialised origin** - scheme, host and port, with no path and no trailing slash; 3. **`Access-Control-Request-Method`** naming the method the real call will use; 4. **`Access-Control-Request-Headers`** naming any header the real call carries that made the browser ask - a browser sends those names lowercased; 5. **no body at all**. Anything you add that a browser would not send, or omit that it would, moves you off the exchange you are trying to explain. ## Judging the answer on two conditions at once A browser applies both of these to the preflight's response, and so must you: | the answer you got | ok status (200-299)? | grant names your Origin? | verdict | |---|---|---|---| | `200` with no `Access-Control-Allow-Origin` | yes | no | fails | | `401` with a correct grant | no | yes | fails | | `301` to a canonical URL | no | - | fails | | `204` with a grant naming your origin | yes | yes | passes | The first row is the one that misleads people, because a green status line reads as success. An `Allow` header on that response is not a CORS grant either - it belongs to `OPTIONS` itself and says nothing about who may read anything. Treat **status alone** and **grant alone** as equally worthless verdicts. ## The traps that make a broken server look healthy - **Omitting `Origin`.** Very many servers emit the grant only when a request carries an `Origin`, so a hand-built preflight without one is answered by a completely different code path and looks fine. This is the single most common way to clear a server that is in fact broken. - **A nearly-right origin string.** A trailing slash, the other scheme, an explicit `:443`, or the host the page *used* to be served from are all different values, and the comparison that matters is exact. - **Testing a different path.** A grant is produced per route far more often than people expect; probing the health endpoint proves nothing about the endpoint that failed. - **Testing through a different network path.** If the page reaches the API through an edge and you reach it directly, you are judging a different server's answer. - **Stopping at the preflight.** A passing preflight only authorises the real request to be sent. ## The second exchange is judged on its own The two exchanges are independent judgements. Once the preflight passes, the browser sends the real request, and **that response is subjected to its own `CORS check`**: it must itself carry `Access-Control-Allow-Origin` naming the calling origin, or the script gets the same empty network error it got before. So the reproduction is not finished until you have sent the real method to the same URL with the same `Origin` and looked at the headers on *that* answer too. This is also how you localise the fault precisely. If the preflight is clean and the real request's response has no grant, the problem is on the main path rather than on whatever answers the preflight - two different places, and the calling side could never have told them apart. ## What you report at the end A finished diagnosis on this leaf names three things: which of the two exchanges failed, which of the two conditions it failed, and what the server returned on the line that shows it. "CORS is broken" names none of them, and the reflex fix that follows - widening the grant to the broadest possible value - is a decision about who may read the API, taken by whoever happened to be on the incident.
- You omit the Origin request header when reproducing the preflight and the answer looks perfect - why is that answer worthless?Because a server that produces its grant only for requests carrying an `Origin` answers a request without one from a different path entirely. You have measured the behaviour a browser never triggers, and cleared a server that is broken for every real caller. The exact origin value is part of the experiment, not decoration.
- The preflight you reproduced answers correctly, yet the console still reads nothing. Where do you look?At the real request's own response. The two exchanges are judged separately, so the grant has to be present on the answer to the actual method as well; a passing preflight only authorises that request to be sent. Send the real method with the same `Origin` and inspect the headers on that answer.
- What do you write in the incident note when you have finished?Which of the two exchanges failed, which of the two conditions it failed, and the response line that shows it - for example, the preflight answered `301` after a canonicalisation rule shipped. That is reproducible evidence; "CORS was broken" is not, and it invites a blind widening of the grant.
saying these in an interview costs you the question
- Reproduces the preflight without the Origin request header.
- Calls a 200 answer proof that the preflight passed.
- Stops at the preflight and never sends the real method.
- Invents an origin string instead of the page's exact one.
- Trusts the rejection message over the wire exchange.
- Probes a health endpoint instead of the failing route.