CORS Preflight Explainer

Describe the request and the response headers, and get a straight answer on whether the browser will let your JavaScript read the response, whether it preflights first, and which header is the reason if not.

The request the browser makes

fetch credentials: include, or withCredentials. This is what makes the wildcard illegal.

The response headers the server sends

cors-decision.txt

updates as you type

    Examples

    Worked setups you can load into the form above. Each one is a decision the generator makes differently, and the reason it makes it.

    The wildcard that blocks a credentialed request

    The commonest CORS failure. The server allowed every origin, and because the request carries cookies the browser rejects the wildcard outright. The error message says nothing about credentials.

    origin
    https://app.example.com
    method
    GET
    credentials
    yes
    allow-origin
    *
    allow-credentials
    yes

    A JSON POST with no OPTIONS answer

    application/json triggers a preflight where the same body as a form post would not. Without Allow-Methods the preflight fails and the real request is never sent, so the server logs show nothing at all.

    origin
    https://app.example.com
    method
    POST
    content-type
    application/json
    allow-origin
    *

    Authorization not listed in Allow-Headers

    It feels like part of the protocol rather than a header you added, which is why it is the one people forget. It both triggers the preflight and has to be named in the response.

    origin
    https://app.example.com
    method
    POST
    content-type
    application/json
    request-headers
    authorization
    allow-origin
    *
    allow-methods
    POST
    max-age
    600

    An echoed origin with no Vary

    A shared cache stores the first origin's response and serves it to the next, which breaks that origin and can hand one tenant a response authorised for another.

    origin
    https://app.example.com
    method
    GET
    allow-origin
    https://app.example.com

    A header the browser will not let you read

    Only seven response headers are readable cross-origin. The rest are visible in the network tab and return null from headers.get(), which reads as the server not sending them.

    origin
    https://app.example.com
    method
    GET
    allow-origin
    *
    read-headers
    x-request-id

    Common mistakes

    These are the ones that fail silently. The config is accepted, nothing raises an error, and the consequence arrives later.

    1. Using a wildcard origin on a request that sends credentials

      The browser rejects `Access-Control-Allow-Origin: *` outright when credentials are involved, so the response is blocked even though the server allowed everyone. The error mentions none of this.

      Instead:Echo the origin literally, set `Access-Control-Allow-Credentials: true`, and add `Vary: Origin`.

    2. Not routing OPTIONS on the path

      A JSON content type triggers a preflight, and a router that only handles POST answers the preflight with 404 or 405. The real request is never sent, so the server logs show nothing at all.

      Instead:Handle OPTIONS on every path a browser calls cross-origin, and set `Access-Control-Max-Age` so it is not repeated per request.

    3. Forgetting to list `Authorization` in Allow-Headers

      It is a non-safelisted request header like any other. It both triggers the preflight and has to be named in the response, and it feels like part of the protocol rather than something you added.

      Instead:List every custom header explicitly. With credentials, `*` is taken literally and matches nothing.

    4. Echoing the origin without `Vary: Origin`

      Any shared cache stores the first origin's response and serves it to the next. That breaks the second origin and can hand one tenant a response authorised for another.

      Instead:Send `Vary: Origin` whenever the allowed origin is computed from the request.

    5. Expecting to read any response header from JavaScript

      Only seven are readable cross-origin. Everything else is on the wire and visible in the network tab while `headers.get()` returns null, which reads as the server not sending it.

      Instead:Add the header to `Access-Control-Expose-Headers`.

    The browser blocked it, and then told you nothing

    A failed CORS check surfaces as a network error with no status and no body, which is why it reads as a server outage from the client and as a successful request in the server's logs. Both are true. The request was sent and processed; the browser then refused to hand the response to your JavaScript.

    A wildcard and credentials cannot be combined

    This is the single most common CORS failure. When the request is made with credentials, the browser rejects Access-Control-Allow-Origin: * outright, even though the server said yes to everyone. The origin has to be echoed literally instead, and once it is echoed the response varies per origin, so it needs Vary: Origin as well. The error message mentions none of this.

    credentials: include  +  Allow-Origin: *              BLOCKED
    credentials: include  +  Allow-Origin: https://app.x  ok
                          +  Allow-Credentials: true
                          +  Vary: Origin

    The preflight is triggered by three things, and JSON is one

    A non-simple method, a header outside the safelist, or a content type that is not form-encoded, multipart or plain text. That last one catches everyone: posting JSON preflights, and posting exactly the same data as a form does not. It cannot be avoided for a JSON API, so the server has to answer OPTIONS on that path. A router that only handles POST returns 404 to the preflight and the real request is never sent.

    Authorization is not a special case

    It is a request header outside the safelist like any other, so it triggers a preflight and it has to be named in Access-Control-Allow-Headers. It feels like part of the protocol rather than something you added, which is why it is the header people forget to list.

    Echoing the origin without Vary poisons shared caches

    A CDN or a corporate proxy stores the response for the first origin that asks and serves it to the next one. That breaks the second origin, and in a multi-tenant setup it can hand one tenant a response that was authorised for another. Any time the allowed origin is computed from the request, the response has to declare that it varies on it.

    Only seven response headers are readable

    Cache-Control, Content-Language, Content-Length, Content-Type, Expires, Last-Modified and Pragma. Everything else is on the wire, visible in the network tab, and returns null from headers.get(), which is why this looks like a server that is not sending the header at all. Access-Control-Expose-Headers is what makes the rest readable.

    What this cannot see

    It makes no requests, so the response headers are the ones you enter rather than the ones your server actually sends. That distinction matters more than it sounds: a proxy, a CDN or a framework middleware can add, replace or strip these headers, and the commonest cause of a CORS problem that survives a correct server config is something in front of the server adding a second Allow-Origin header. Read the real ones from the network tab, then bring them here. It also does not model private network access or the older allow-list behaviour of some CDNs.