DEV Community

Jeff
Jeff

Posted on Originally published at powerduck.com

Why a CORS Preflight OPTIONS Request Returns 401

Quick answer: A 401 on the CORS preflight means an auth layer rejects OPTIONS before the CORS layer handles it. The browser sends OPTIONS without the Authorization header or cookies on purpose, so middleware that demands a token on every method blocks it. Answer OPTIONS anonymously with the CORS headers and keep auth on the real request.

Why the preflight carries no credentials

Before a cross-origin request that is not simple, such as one with application/json, an Authorization header, or a custom header, the browser sends an OPTIONS first. The preflight asks permission and carries none of the real request's data.

Part of the exchange Present on the preflight
Origin Yes
Access-Control-Request-Method Yes
Access-Control-Request-Headers Yes, when custom headers are used
Authorization header No
Cookies No

This is defined behavior, not a missing header. The browser will not attach a token to OPTIONS, so a server that waits for one can never succeed from the browser, even though the endpoint works in a tool sending the token directly.

What the 401 is telling you

A 401 on OPTIONS means auth ran first and CORS never got a chance. Trace where the status is produced, since the fix differs by layer.

Layer that returns 401 Typical signature Where to look
App auth middleware or global guard App's own error body, framework headers Middleware order, method allow-list
Reverse proxy Server header, proxy error format auth_request or location rules
API gateway authorizer Gateway request ID, no app body Authorizer attached to all methods
Edge function Edge-specific headers Function order before CORS logic

A WWW-Authenticate header confirms an auth challenge fired. Match the request ID to the layer that logged it rather than guessing from the generic "Failed to fetch."

Why it happens

The common cause is ordering: auth middleware registered before CORS middleware makes every request, including OPTIONS, hit auth first, and a framework guard protecting all routes does the same unless OPTIONS is excluded.

At the edge, a reverse proxy auth_request or cloud authorizer often applies to every method and challenges OPTIONS before the app sees it. Path-level rules can protect the URL for every verb, leaving no anonymous route. These are not CORS header problems; more Access-Control-* headers will not help a request rejected first.

How to isolate it

Reproduce the preflight with a token-free probe, mirroring the browser:

curl -i -X OPTIONS 'https://api.example.com/widgets' \
  -H 'Origin: https://app.example.com' \
  -H 'Access-Control-Request-Method: POST' \
  -H 'Access-Control-Request-Headers: authorization,content-type'
Enter fullscreen mode Exit fullscreen mode

A 401 here reproduces the fault outside the browser. Send the same probe with a valid token; if it returns 204, the server is gating OPTIONS on auth. Match the headers and request ID with logs to name the layer and check the middleware order.

The fix

Handle OPTIONS before auth and short-circuit it with the CORS headers, then let the real request pass through auth.

Do Why
Answer OPTIONS anonymously with 200 or 204 The preflight only asks permission
Register CORS handling ahead of auth So the preflight is never challenged
Add OPTIONS to the gateway's bypass list Edge auth should not block the probe
Keep auth on the real method Access control still protects the resource
Echo the specific origin with credentials A wildcard * cannot be combined with credentials

Answering OPTIONS anonymously does not open a hole. The handler returns headers describing what is allowed, not protected data, and the real GET or POST still needs a token. Confirm the real response also carries CORS headers, since a successful OPTIONS alone is not enough.

Checking the API without browser CORS

You can separate a CORS fault from an API fault with a desktop API workspace. Its requests go through the local main process rather than a browser page, so the browser CORS model does not block them, and you can confirm the endpoint returns data with a valid token while the preflight fails. Powerduck works this way and keeps the request and response as evidence, showing the API logic is sound and the remaining problem is CORS and auth ordering. For the rest of the series, see the API debugging guide hub.

Honest limits

A 401 narrows the symptom but does not name the layer; the same status can come from app middleware, a proxy, or a gateway authorizer, so logs and request IDs stay the source of truth. A desktop request that bypasses browser CORS proves the API answers, not that your CORS setup is correct. The final check belongs in a real browser, including an expired-token test to see an expected error carry the right headers.

Frequently asked questions

Can I make the preflight send the token? No. The browser builds the preflight and leaves out credentials. Working around the client is the wrong direction; the server should let OPTIONS pass.

Is letting OPTIONS through anonymous unsafe? No. OPTIONS returns CORS headers and no resource data. The real request still needs authentication, so access control is unchanged.

Why does it work in an API client but fail in the browser? A desktop client sends the request directly with the token. The browser sends a credential-free OPTIONS first, which the auth layer rejects.

The bottom line

Read a 401 on OPTIONS as an ordering fault: the preflight carries no credentials by design, so move CORS handling and the OPTIONS bypass ahead of authentication at every layer, then leave auth firmly on the real request. Reproduce it with a token-free probe, name the layer from the request ID and headers, and confirm the result in a real browser rather than treating a desktop client's success as proof the CORS setup is right.

Top comments (0)