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'
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)