Home / Blog / URL HTTP & cURL

API & webhooks

CORS Headers Explained: Request, Preflight and Response

Separate CORS request headers from response permissions, identify the preflight and actual-response stages, and diagnose failures using the complete exchange.

Read CORS as an exchange between browser and server

CORS is a browser-enforced sharing mechanism involving request context and server response fields. A list of header names is easier to understand when each field is placed in the correct message. Adding a response permission to an outgoing request does not grant the browser permission to read the server's response.

Begin with the application's origin, the target URL, the request method, its header names and whether credentials are included. Those details determine which parts of the policy matter. The same response block can have different implications for different requests.

Separate the fields by direction and purpose

FieldWhere it appearsQuestion it addresses
OriginBrowser requestWhich initiating origin is supplied?
Access-Control-Request-MethodPreflight requestWhich method does the browser propose?
Access-Control-Request-HeadersPreflight requestWhich request-header names need permission?
Access-Control-Allow-OriginServer responseWhich origin may read under this response policy?
Access-Control-Allow-MethodsPreflight responseWhich method tokens are permitted?
Access-Control-Allow-HeadersPreflight responseWhich request-header names are permitted?
Access-Control-Expose-HeadersActual responseWhich additional response fields can code read?

Access-Control-Allow-Credentials adds a separate credentialed-sharing condition. Access-Control-Max-Age concerns how long a preflight permission can be cached, subject to browser limits. It is not the freshness lifetime of the API's JSON body.

Trace one JSON write through both stages

Imagine a JSON POST with X-Trace from https://app.example.com to https://api.example.com/notes. The browser first asks about POST and the relevant request-header names. If the preflight is rejected, the browser does not proceed with that proposed actual request.

If the preflight passes, the POST can run and the API returns its application response. That response needs the applicable sharing policy too. A readable 400 validation response can be more useful than a generic browser error that hides the server's explanation because the error path omitted CORS fields.

The MDN CORS overview describes these stages. A successful preflight should therefore be logged as permission for the proposed request, not as proof that the endpoint operation itself succeeded.

Locate the stage that failed

When debugging, find the OPTIONS exchange if one was required, then find the actual request. If only OPTIONS exists, compare the requested method and header names with the preflight response. If the actual request exists, inspect its status and sharing fields separately.

A network failure can have a different cause, such as DNS, TLS or a disconnected client. JavaScript's rejected fetch promise does not identify every underlying cause precisely. Use the browser's diagnostic details and a controlled reproduction before changing a server policy.

The Allow-Headers reference explains request-header permissions. Do not confuse a missing permission there with a missing exposure rule for a returned custom header; the fixes belong to different response fields.

Repeat tests without misreading cached permissions

A previously permitted preflight may be reused, so a later test need not visibly send OPTIONS every time. Record the browser context and policy changes when comparing traces. A fresh test context can help isolate whether cached permission influenced the observation.

For example, changing only a response body should not require a new permission experiment. Changing the requested custom-header set is a different case and should be tested as such. Keep these cases separate so a missing OPTIONS row is not automatically treated as a broken test.

Keep token review separate from the browser verdict

The CORS Header Checker preserves pasted empty and duplicate fields and compares the supplied origin and method token. It does not know the full credential mode, request-header list or both response stages, so it reports observations rather than promising a browser outcome.

Use one labelled block per review, retain its stage and request context, and verify changes in a real local browser fixture. A complete reproduction should show which operation was attempted, whether the actual request reached the server and which response data became readable.