skip to content

In a middleware chain, why must the CORS hook sit ahead of authentication rather than behind it?

level: middleimportance: should knowfreq 55%

answer

  1. the preflight carries no credentials
  2. a challenge without the headers reads as blocked
  3. answered before any credential check
  4. decoration must wrap the error stage
  5. successes fine, failures look cross-origin

basics

~20 s

A browser sends the preflight without credentials, so an authentication hook placed ahead of the CORS hook answers it with a 401 carrying no CORS headers, and the browser blocks the real request. CORS must run first.

solid answer

~40 s

The CORS hook has two jobs and both constrain its depth. It answers the browser's preflight itself, and it decorates real responses with the CORS headers. The preflight is deliberately unauthenticated — no cookies, no authorization header — so if authentication wraps the CORS hook, the preflight is answered `401`, that response carries no CORS headers, and the browser reports a CORS failure and never sends the real request. Placing CORS outside authentication fixes it: the preflight is answered and short-circuited before any credential check runs. The decorating half has the same constraint against the error stage — it must wrap it, so that a `500` still carries the headers. Otherwise a genuine server error reaches the page as an unexplained CORS error, and the developer debugs the wrong layer.

go deeper

for a junior

Remember that a browser asks permission first with a separate anonymous request, and that this request must be answered before anything tries to identify the caller.

for a middle

Explain the mechanism: the preflight carries no credentials, and a rejection emitted without the response headers is reported to the page as a cross-origin failure rather than as the rejection it is.

for a senior

Diagnose from evidence — only an OPTIONS in the logs, or successes fine while failures surface as cross-origin errors — and name which hook is wrapping which in each case.

for a principal

Own the invariant across services: one place decides origin policy, it wraps the error stage everywhere, and no team can make an endpoint browser-unreachable by adding a hook at the wrong depth.

## Two jobs in one hook A CORS hook does two separate things, and it helps to keep them apart when reasoning about placement: 1. **Answer the preflight.** For cross-origin requests that are not simple, a browser first sends a separate `OPTIONS` request asking whether the real request would be allowed. The hook recognises it, answers it (typically `204`) with the relevant headers, and does not delegate further. 2. **Decorate real responses.** For the actual request, the hook delegates and then adds the CORS response headers to whatever comes back. The first job decides how far **outward** the hook must sit relative to authentication. The second decides how far outward it must sit relative to the error stage. ## Why authentication cannot come first A preflight is, by design, a bare question about policy. The browser sends it without cookies and without an authorization header, and it will not retry it with credentials attached. So an authentication hook that wraps the CORS hook sees an anonymous request and does the correct thing for an anonymous request: it rejects it. That rejection is the problem, and not for the reason people first assume. The failing status is secondary; what breaks the page is that **the rejection response carries no CORS headers**, because the hook that would have added them never ran. To the browser this is indistinguishable from a server that does not permit the origin. The page sees a CORS error, the real request is never sent, and the server-side logs show only an anonymous `OPTIONS` that was refused — an unremarkable line that nobody connects to the bug report. Placing the CORS hook outside authentication resolves it directly: the preflight is recognised and answered before any credential check is reached, and the real request that follows still passes through authentication normally. Nothing is weakened, because a preflight carries no data and performs no action — it asks a policy question and receives a policy answer. ## Why the decorating half must wrap the error stage The same placement logic applies on the way out. If the error stage sits outside the CORS hook, then any response the error stage produces — an unhandled exception, a validation failure, a timeout — is built after the decoration point and goes out without the headers. The symptom is one of the most misleading in web development: a `500` that the browser reports to the page as a CORS error. Front-end and back-end engineers then spend an afternoon on cross-origin configuration while the actual defect is a null dereference in a handler. Keeping the CORS hook outside the error stage means every response — success, client error, server error — leaves decorated, so a failure looks like a failure. ## A placement summary | Hook | Relative to CORS | Why | |---|---|---| | Correlation id | outside | every record, including refused preflights, needs the id | | Access log, metrics | outside | preflights and CORS failures must still be counted | | Error stage | inside | so error responses are decorated too | | Authentication | inside | a preflight carries no credentials and must not be challenged | | Handler | inside | it never sees the preflight at all | ## Failure signatures worth recognising - **The real request never appears in the logs, only an `OPTIONS`.** The preflight was refused; look at what is wrapping the CORS hook. - **Successful responses are fine, failures report as CORS errors.** The decorating half is inside the error stage. - **It works from a test client but not from a browser.** A non-browser client sends no preflight and ignores the response headers, so it exercises a different path entirely; this is why an endpoint can look healthy from the command line and be unusable from a page. - **Only some routes fail.** The hook is attached per-route or per-group, and the failing routes were not covered. ## The general lesson This is the clearest instance of a rule that runs through the whole chain: **a hook that answers a request on behalf of the protocol must sit outside any hook that would reject that request first, and a hook that decorates responses must sit outside every element that can produce one.** Read the preflight's own requirements — anonymous, answered directly, must carry headers even on failure — and its depth is determined, not chosen.

  • A page reports a CORS error, but the server logged a 500 for that route. What does that tell you about placement?
    The response-decorating half of the CORS hook is inside the error stage, so the response the error stage produced left without the headers. The browser cannot distinguish that from a disallowed origin. Move the CORS hook outside the error stage and the same failure will surface as the `500` it actually is.
  • Should preflight requests appear in the access log?
    Yes. The observation hooks sit outside the CORS hook, so a preflight that is answered and short-circuited is still counted and logged. That line is often the only evidence available when a browser reports a cross-origin failure but the real request was never sent.

saying these in an interview costs you the question

  • Thinks the browser retries a preflight once credentials are attached
  • Believes the status matters more than the missing response headers
  • Puts the CORS hook inside authentication and blames browser behaviour
  • Tests only with a non-browser client, which sends no preflight
  • Leaves error responses undecorated and debugs cross-origin config instead