Browser / API Request Debugging workflow

How to Debug CORS Preflight Failures

Work through CORS preflight failures by checking OPTIONS responses, allowed origins, requested headers, methods, credentials and proxy behavior.

Quick Answer

A CORS preflight failure means the browser asked the server whether a cross-origin request is allowed, and the server did not answer with matching CORS headers. Inspect the OPTIONS request, requested method, requested headers, Origin and credentials behavior before changing application logic.

Example Scenario

A POST request works in curl but fails in the browser with a CORS error. The API endpoint is reachable, the server may even process similar requests, but the browser blocks the actual request because the preflight OPTIONS response does not allow the method, origin, headers or credentials the frontend wants to use.

Step-by-Step Explanation

  1. Find the OPTIONS request in the Network panel before inspecting the failed main request.
  2. Check the Origin request header and Access-Control-Allow-Origin response header.
  3. Compare Access-Control-Request-Method with Access-Control-Allow-Methods.
  4. Compare Access-Control-Request-Headers with Access-Control-Allow-Headers.
  5. Check whether credentials require a specific origin and Access-Control-Allow-Credentials.
  6. Verify that proxies and error handlers add CORS headers to preflight and error responses.

CORS Is Enforced by the Browser

CORS can feel like a server availability problem because the browser shows a failed request. In many cases the API is reachable. Curl, server-to-server jobs and backend tests may all succeed. The browser is the component enforcing cross-origin rules before exposing the response to frontend code.

That distinction changes the debugging path. Do not start by rewriting fetch or changing JSON parsing. Start with the browser Network panel and find the preflight OPTIONS request. The preflight response is where the server either grants or denies the browser permission to send the real request.

If the OPTIONS request is missing, the request may be simple enough not to require preflight. If OPTIONS appears and fails, focus on CORS headers, methods, requested headers, credentials and response status.

Match Origin Exactly When Credentials Are Used

For credentialed requests, Access-Control-Allow-Origin cannot be a wildcard. The server must echo or specify the exact allowed origin, and it must include Access-Control-Allow-Credentials: true. Cookies, authorization headers and TLS client certificates all push the request into a stricter path.

Origin matching is exact enough that scheme, host and port matter. http://localhost:3000 and http://localhost:5173 are different origins. https://app.example.com and https://www.example.com are different origins. A missing port in local development can explain why one teammate succeeds and another fails.

During debugging, log the Origin header seen by the server and the Access-Control-Allow-Origin header sent back. If they do not line up, the browser will block the request even when the endpoint itself works.

Be careful with origin allowlists that perform loose string checks. Allowing every origin that ends with example.com can accidentally include attacker-controlled domains such as badexample.com. A debugging fix should still preserve exact matching rules.

Requested Headers Must Be Allowed

Preflight includes Access-Control-Request-Headers when the actual request wants to send non-simple headers. Common examples include Authorization, Content-Type with application/json, x-api-key and custom trace headers. The server must allow the requested headers by name.

A common failure happens when a frontend adds a harmless-looking header for observability or versioning. The backend route accepts the main request, but the CORS middleware does not list the new header. The actual request is never sent because preflight fails first.

Compare the requested header list with the allowed header list case-insensitively, but keep the configured names readable. Avoid allowing every header by default unless that matches the security model for the API.

The Method Must Be Allowed Too

Access-Control-Request-Method tells the server which method the real request will use. If the frontend sends PATCH but the preflight response only allows GET and POST, the browser blocks it. This can happen when an endpoint adds a new method but CORS configuration is not updated.

Method failures are easy to miss because the browser console may show a general CORS message. The Network panel is clearer: inspect OPTIONS, then read the allow-methods header. If the method is absent, the fix belongs in server or proxy configuration.

Also confirm that OPTIONS itself is routed correctly. Some servers return 404 or 405 for OPTIONS because only business routes were registered. Preflight needs a valid response even when the main route exists.

Proxies and Error Responses Can Strip CORS Headers

A CORS setup can work for successful responses and fail for errors. If an auth layer, gateway or server error path returns without CORS headers, the browser reports a CORS problem instead of exposing the real 401, 403 or 500 response. That makes the root cause harder to see.

Check whether preflight reaches the application server or is answered by a proxy. CDN rules, reverse proxies and platform edge functions may handle OPTIONS before the request reaches application code. If the wrong layer responds, application middleware changes will not help.

A strong configuration adds CORS headers consistently to preflight and to relevant error responses. That lets the browser expose useful API errors instead of collapsing every failure into a blocked cross-origin message.

What to Check Next

After the preflight passes, inspect the actual request and response. CORS success only means the browser is allowed to make the request and read the response. The API can still return 401, invalid JSON, a validation error or an HTML error page.

Keep a small preflight checklist for each environment: origin, method, requested headers, allowed headers, credentials and status. Environment-specific CORS bugs are common because local hosts, staging domains and production domains differ.

Avoid fixing CORS by switching to no-cors mode. That produces an opaque response that frontend code cannot read, which usually hides the problem rather than solving it.

Also check caching. Browsers can cache preflight responses according to Access-Control-Max-Age. If you change server configuration and the browser still fails, retry in a fresh session or clear the relevant cache before assuming the fix did not work.

Code Examples

A request likely to trigger preflight
await fetch('https://api.example.com/orders', {
  method: 'POST',
  credentials: 'include',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer token'
  },
  body: JSON.stringify({ item: 'book' })
});
Typical preflight response headers
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET, POST, PATCH
Access-Control-Allow-Headers: content-type, authorization
Log the origin and requested headers on the server
app.options('/api/orders', (req, res) => {
  console.log('origin:', req.headers.origin);
  console.log('method:', req.headers['access-control-request-method']);
  console.log('headers:', req.headers['access-control-request-headers']);
  res.sendStatus(204);
});

Common Mistakes

  • Debugging the main request before inspecting OPTIONS.
  • Using wildcard Access-Control-Allow-Origin with credentials.
  • Forgetting to allow Authorization or custom headers.
  • Returning 404 or 405 for OPTIONS on a valid API route.
  • Using no-cors mode and expecting to read the response body.

FAQ

Why does curl work when the browser fails?

Curl does not enforce browser CORS rules. The browser blocks cross-origin access unless the server grants permission with matching headers.

Do all cross-origin requests have preflight?

No. Simple requests may skip preflight. Methods, headers and content types determine whether the browser sends OPTIONS first.

Can I use * with credentials?

No. Credentialed browser requests require a specific allowed origin and Access-Control-Allow-Credentials: true.

Is no-cors a fix?

Usually no. It creates an opaque response that frontend code cannot inspect, so it rarely helps API debugging.