CORS preflight failed: 405, “does not have HTTP ok status” and friends
Before some requests the browser asks the server for permission with an OPTIONS request. If that question gets a 405, a login page or a redirect, the real request is never sent.
What a preflight is
For a simple request (GET or POST with plain form or text bodies and no custom headers) the browser just sends it. Anything else, such as PUT/DELETE, a JSON Content-Type, or an Authorization header, triggers a preflight: an OPTIONS request asking whether the real one is allowed.
OPTIONS /api/items HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: authorization, content-type
The server must reply with a 2xx and matching headers:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 7200
Vary: Origin
If anything is wrong the browser shows a CORS error and your fetch rejects with TypeError: Failed to fetch. The start of the story is in the CORS errors guide; this page is about the preflight step.
The messages, decoded
| Browser says | Usual cause | Fix |
|---|---|---|
| It does not have HTTP ok status | OPTIONS got 404, 405, 401, 403 or 500 | Add an OPTIONS handler that returns 204 before routing and auth run |
| Redirect is not allowed for a preflight request | http to https, a trailing slash (/api/items to /api/items/) or a login redirect | Call the final URL exactly; don’t redirect OPTIONS |
| Method PUT is not allowed by Access-Control-Allow-Methods | The method list is missing it | Add the method |
| Request header field authorization is not allowed by Access-Control-Allow-Headers | Header not listed | List every custom header the page sends |
| No ‘Access-Control-Allow-Origin’ header is present (on the OPTIONS) | CORS headers only added to GET/POST responses, or only on success | Add them to OPTIONS and error responses too |
| The value of … must not be the wildcard ‘*’ when credentials mode is ‘include’ | * with cookies | Echo the exact allowed origin and send Access-Control-Allow-Credentials: true |
Need a reminder of what each status means? Look up any HTTP status code.
The 405 and 401 traps
- 405 Method Not Allowed: the framework routes by method and you only defined
PUT /api/items, soOPTIONS /api/itemsmatches nothing. Many frameworks need a CORS middleware or an explicit OPTIONS route. - 401 Unauthorized: preflights never carry your
Authorizationheader or cookies, by design. If an auth middleware runs first, it rejects every preflight. Let OPTIONS through before auth. - Proxies and gateways: a load balancer, API gateway or CDN rule may block OPTIONS or strip headers before your app sees it.
curlfrom the next section shows who answers.
Test the preflight with curl
curl -si -X OPTIONS https://api.example.com/api/items \
-H "Origin: https://app.example.com" \
-H "Access-Control-Request-Method: PUT" \
-H "Access-Control-Request-Headers: authorization, content-type"
You want a 204 (or 200) and the four Access-Control-Allow-* lines. A 301/302 with a Location means a redirect, so fix the URL (see redirects explained).
Answering preflights by stack
# nginx (inside the location that proxies the API)
if ($request_method = OPTIONS) {
add_header Access-Control-Allow-Origin "https://app.example.com" always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE" always;
add_header Access-Control-Allow-Headers "Authorization, Content-Type" always;
add_header Access-Control-Max-Age 7200 always;
return 204;
}
// Express: before routes and auth
const cors = require('cors');
app.use(cors({ origin: 'https://app.example.com', credentials: true }));
app.options('*', cors()); // Express 4; Express 5 answers preflights via app.use
# Flask
from flask_cors import CORS
CORS(app, origins=["https://app.example.com"], supports_credentials=True)
# FastAPI
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(CORSMiddleware, allow_origins=["https://app.example.com"],
allow_methods=["*"], allow_headers=["*"], allow_credentials=True)
Serverless functions: handle event.httpMethod === "OPTIONS" (or the platform’s equivalent) first and return 204 with the headers; also enable CORS on the gateway itself if it has a setting.
Or avoid the preflight
- Serve the page and API from the same origin (a reverse proxy rule such as
/apion the same domain), so no CORS applies at all. In development, the dev server’s proxy option does this. Access-Control-Max-Agelets browsers cache the answer (Chrome caps it at 2 hours) so the extra request happens rarely.
Preflight passes but cookies still go missing? See CORS with credentials.
Questions
What is a CORS preflight request?
An OPTIONS request the browser sends before a cross-origin request that uses methods like PUT or DELETE, a JSON Content-Type or custom headers like Authorization. The server must answer with a 2xx status and Access-Control-Allow-* headers, or the real request is not sent.
Why does my preflight return 405?
The server or framework has no handler for the OPTIONS method on that URL, so it answers 405 Method Not Allowed. Add CORS middleware or an OPTIONS route that returns 204 with the CORS headers.
What does 'It does not have HTTP ok status' mean?
The preflight OPTIONS response was not 2xx: commonly 401 because auth ran on a request that never carries credentials, 404 or 405 because no route matched, or 500. Let OPTIONS through before authentication and return 204.
Why is a redirect not allowed for a preflight request?
Browsers refuse to follow redirects on preflights. Call the exact final URL (https, correct trailing slash) and make sure no login or HTTPS redirect applies to OPTIONS.
Published .