Distributed systems

CORS preflight failure — inspect the OPTIONS response first

Written and reviewed by Sahil Srivastav

HTTPBrowser securityAPI integration
No 'Access-Control-Allow-Origin' header is present on the requested resource.

What this error actually means

The browser could not establish permission for a cross-origin script to access the response. The quoted console fragment is real; the surrounding wording varies by browser. An origin consists of scheme, hostname and port, so a different development port is a different origin even when both processes run on the same laptop.

Some requests require a preflight: the browser sends OPTIONS describing the intended method and headers before sending the actual request. A successful preflight must allow that origin and request shape. The eventual response must also carry the required CORS headers. Passing OPTIONS does not make a later 401 or 500 automatically readable by the browser.

The diagnostic trap is that CORS can hide another failure. A gateway-generated 502 without CORS headers looks like a CORS problem in JavaScript, while the real API never answered. Conversely, curl succeeding proves reachability but does not prove browser access: curl does not enforce the browser’s CORS checks. Inspect the wire response and the application failure separately.

Causes, most common first

  1. 1Authentication runs before the preflight handler. The preflight describes a future request; it generally does not carry the application credentials expected by the protected route. Rejecting OPTIONS as an unauthenticated business operation prevents the browser from attempting the authenticated request at all.
  2. 2The allowed origin does not match the actual origin. A scheme change, development port or preview hostname escapes the configured allowlist. Reflecting every supplied Origin fixes the symptom by discarding the policy; use an explicit trust decision instead.
  3. 3Error responses bypass the CORS layer. Only the successful route handler adds headers. Exceptions, framework authentication responses and proxy errors leave through other paths, causing misleading console failures when the underlying request encounters trouble.
  4. 4Credential or cache policy is inconsistent. Credentialed browser requests cannot use a wildcard allowed origin. A response whose allowed origin changes with the request also needs appropriate cache variation, or a shared cache can return another origin’s policy.

When you see it

  • A request works with curl or a server-side client but fails from browser JavaScript.
  • The Network panel shows OPTIONS returning 401, 403, a redirect or a gateway error.
  • Requests without custom headers work, while JSON POST requests trigger the failure.
  • One frontend origin works until a cached response produced for another origin is reused.

How to diagnose it

Step 1

Reproduce the browser’s preflight precisely

Use the actual frontend origin, method and requested-header list from DevTools. This command only inspects the server’s answer; it does not implement browser enforcement. Confirm a successful status and all the necessary allow headers.

curl -i -X OPTIONS https://api.example.com/orders -H 'Origin: https://app.example.com' -H 'Access-Control-Request-Method: POST' -H 'Access-Control-Request-Headers: authorization,content-type'

Step 2

Inspect the real response as well as OPTIONS

Use a safe read endpoint or a disposable test resource. A missing header on a 401 tells you the error path is outside the policy layer. Correlate gateway errors with upstream logs before changing the origin allowlist.

curl -i https://api.example.com/orders -H 'Origin: https://app.example.com'

Step 3

Compare cache and proxy layers

Inspect Vary, cache-hit indicators and duplicate Access-Control-Allow-Origin headers. Two layers each adding a header can produce an invalid combined value. Repeat from two allowed test origins to expose an incorrectly shared cached policy.

Step 4

Verify that the browser sends the intended request

Look for OPTIONS and then the actual request in the Network panel. If only OPTIONS appears, concentrate on preflight. If the POST appears and changes state, diagnose its response path and avoid repeating the mutation blindly.

The fix

Put the CORS policy at a consistent request boundary and handle preflight before business authentication. Validate the origin, requested method and requested headers against explicit allowlists. Returning a successful OPTIONS response does not authorise the subsequent business request; retain authentication and authorisation there.

For credentialed calls, return the exact approved origin and Access-Control-Allow-Credentials: true, and configure the browser’s credentials mode deliberately. Cookie SameSite and Secure rules remain independent requirements. A CORS fix cannot force a browser to send a cookie prohibited by its cookie policy.

Apply the policy to readable application error responses as well as successes, while giving one layer clear ownership of the headers. If nginx adds headers, understand its add_header status handling and the always parameter. Do not create duplicate origin headers by also adding them unconditionally in the application.

When the origin response varies, merge Origin into Vary without deleting existing values such as Accept-Encoding. Bound preflight cache duration during rollout so policy mistakes are observable. Validate both an allowed origin and a deliberately disallowed origin before calling the incident resolved.

How to stop it coming back

  • Test allowed and rejected origins across successful responses, authentication failures, validation errors and dependency failures.
  • Keep preview-domain allowances explicit and narrow; avoid substring checks that accidentally trust attacker-controlled hostnames.
  • Document ownership of CORS headers across CDN, ingress and application so a configuration change does not double them.

Practise production debugging in a real repository

Reading about a failure and reproducing one are different skills. Gronex ships broken backend repositories with failing test suites that encode the real invariant, so you debug from evidence instead of memorising symptoms.

FAQ

Can I fix this with fetch mode: no-cors?

That produces an opaque response for applicable requests: JavaScript cannot read its status, headers or body. It is not a solution for an API whose JSON response your application needs to inspect.

Does CORS prevent unauthorised API calls?

No. It controls browser script access to responses, not whether arbitrary HTTP clients can contact the API. Keep server-side authentication, authorisation and any needed CSRF protection; a permissive curl client is outside this browser policy.

Why did my mutation succeed even though the browser reported CORS?

The request may have been sent successfully and only its response failed the access check. Check the operation’s state before retrying. CORS failure is not evidence that the server skipped or rolled back the write.

Related

Other errors engineers hit next to this one

Full error and symptom index →