What Is a CORS Preflight Request (OPTIONS)? A Cross-Origin Mechanism Every Frontend Developer Must Know
You call an API from the browser. The URL looks fine, the payload looks fine, but the console throws a red error:
Access to fetch at 'https://api.example.com/data' from origin 'http://localhost:3000'
has been blocked by CORS policy: Response to preflight request doesn't pass
access control check: No 'Access-Control-Allow-Origin' header is present.
Worse: the Network panel shows an OPTIONS request you never wrote. That OPTIONS call is the CORS preflight. This article breaks down simple request vs preflight, the Allow-* response headers, credentials mode, and common pitfalls. To compare real responses, use the API Tester (browser direct is still subject to CORS; server proxy bypasses the browser's CORS checks) and the HTTP Status Codes lookup.
Same-origin, CORS, and preflight — keep the layers separate
The same-origin policy means a page can only freely read responses from the same scheme, host, and port. CORS (Cross-Origin Resource Sharing) is how a server tells the browser, via response headers, that a cross-origin response may be exposed to frontend script.
A preflight is not “an extra business call.” Before a non-simple request, the browser sends OPTIONS to ask whether the method and headers are allowed. If preflight fails, the actual request is never sent — which is why you often see a failed OPTIONS and no POST/PUT on the business path.
For Token + 401 + 405 stacked on CORS, see API testing: tokens and CORS errors.
Simple request vs preflight — trigger rules in detail
Spec language uses CORS-safelisted methods and headers. A request is “simple” (no preflight) only when all of the following hold:
| Dimension | Simple-request requirement |
|---|---|
| Method | Only GET, HEAD, or POST |
| Headers | Only safelisted headers (e.g. Accept, Accept-Language, Content-Language, plus a restricted Content-Type) |
Content-Type (if set) | Only text/plain, multipart/form-data, or application/x-www-form-urlencoded |
Any break triggers a preflight. The usual frontend traps:
- Methods such as
PUT,DELETE,PATCH - Custom headers:
Authorization,X-API-Key,X-Requested-With, … Content-Type: application/json(almost every JSON API)- Other non-safelisted headers or non-safelisted values on otherwise safelisted headers
// Preflight: JSON Content-Type
fetch('https://api.example.com/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'test' }),
});
// Preflight: non-simple method
fetch('https://api.example.com/users/1', { method: 'DELETE' });
// Preflight: custom header
fetch('https://api.example.com/data', {
headers: { 'X-API-Key': 'abc123' },
});
Rule of thumb: classic form POSTs are often simple; modern fetch + JSON + Bearer tokens almost always preflight.
What the preflight sends — and what the server must return
The browser’s automatic OPTIONS looks roughly like:
OPTIONS /users HTTP/1.1
Host: api.example.com
Origin: http://localhost:3000
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type, authorization
| Request header | Meaning |
|---|---|
Origin | Page origin (scheme + host + port) |
Access-Control-Request-Method | Intended business method |
Access-Control-Request-Headers | Non-simple headers that will be sent (comma-separated; case-insensitive) |
A typical approving response (status often 204 or 200; see the HTTP status codes guide):
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: http://localhost:3000
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400
Vary: Origin
Access-Control-Allow-* cheat sheet
| Response header | Role | Common pitfall |
|---|---|---|
Access-Control-Allow-Origin | Which origin may read the response (* or a concrete origin; see credentials) | Missing; or production origin hardcoded while you debug on localhost |
Access-Control-Allow-Methods | Allowed methods | Only GET, POST while the app uses PUT/DELETE |
Access-Control-Allow-Headers | Allowed request headers | Forgot Authorization / Content-Type |
Access-Control-Max-Age | Seconds to cache the preflight result | Stale cache after you fix the server |
Access-Control-Allow-Credentials | Whether cookies / credentialed mode is allowed | Incompatible with Allow-Origin: * |
Access-Control-Expose-Headers | Which response headers JS may read via getResponseHeader | Allow-* fixed but custom response headers still invisible |
During preflight the browser mainly checks: Does Allow-Origin cover the current Origin? Does Allow-Methods include Request-Method? Does Allow-Headers cover every Request-Headers entry? Any mismatch → no business request.
The actual response (after a successful preflight) still needs a correct Access-Control-Allow-Origin (and Allow-Credentials when applicable). Fixing OPTIONS alone while omitting CORS headers on the real response still fails at “request left, body unreadable.”
Credentials mode: cookies and * cannot coexist
With:
fetch(url, { credentials: 'include' });
// or XHR: xhr.withCredentials = true
the browser may send cookies (and related auth state). CORS tightens:
- Server must send
Access-Control-Allow-Credentials: true Access-Control-Allow-Originmust not be*; it must be an exact concrete origin (e.g.https://app.example.com)- Both preflight and the actual response must satisfy the above
Typical console text: The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'.
Practice: match Origin against a whitelist, echo that origin, and add Vary: Origin so CDNs do not serve origin A’s response to origin B.
Common pitfalls (by how often they show up)
1. OPTIONS returns 405 / 404
Routes register POST /users but not OPTIONS /users. The browser hits 405 Method Not Allowed and preflight dies. Look up codes in the HTTP Status Codes tool. Fix: handle OPTIONS on the gateway or app (or use a solid CORS middleware).
2. Assuming “it’s just a POST”
POST can be simple — until you set JSON Content-Type. curl/Postman work; the browser fails. That gap is usually this layer.
3. Allow-Origin without Allow-Headers / Methods
Preflight asks for Authorization; the response never lists it → Request header field Authorization is not allowed by Access-Control-Allow-Headers.
4. Gateway strips CORS headers
The app adds headers; Nginx / CDN / API Gateway short-circuits OPTIONS or overwrites the response. Trust the headers the browser actually received, not only app logs.
5. Max-Age caches a bad preflight
You fix CORS; the browser still uses a cached preflight answer. Disable cache, use a private window, or shorten Max-Age in development.
6. Redirects across origins
A cross-origin 302 to another host makes CORS checks stricter along the redirect chain. Confirm the final URL and hop statuses (the API Tester helps inspect status codes and redirect behavior).
7. Mixed content mistaken for CORS
An HTTPS page requesting an http:// URL is blocked before send — not a CORS failure. The site’s API Tester separates this failure mode in direct mode so you do not chase the wrong server headers.
How to triage with tools on this site
- Pick the mode: the API Tester switches between browser direct and server proxy.
- Direct: same constraints as page
fetch(CORS / mixed content) — reproduce real frontend failures. - Proxy: the site’s server forwards the request — no browser same-origin preflight against the target; good for “does the API itself work?”
- Direct: same constraints as page
- Read the status: preflight often
204/200; failures often403/404/405. Use HTTP Status Codes when unsure. - Send OPTIONS yourself: choose OPTIONS in the tester and inspect whether
Access-Control-Allow-*is complete. - When Token + 401 stack on CORS: clear preflight first, then auth — see tokens and CORS.
Production still needs real CORS config. Proxy mode is a debug aid; end users’ browsers will not use your debug proxy.
Temporary workarounds when you cannot change the server
- Dev proxy: Vite / webpack
devServer.proxyso/apiis same-origin to the browser. - This site’s server proxy: enable proxy in the API Tester to validate API logic.
- Browser extension to disable CORS: local development only — never for daily browsing.
// Vite proxy example
export default {
server: {
proxy: {
'/api': {
target: 'https://api.example.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
},
};
Bottom line
Non-simple request → browser OPTIONS preflight → server approves via Access-Control-Allow-* → then the business request; the business response needs CORS headers too. In Network, align Access-Control-Request-* with Access-Control-Allow-*. To confirm the API works, use the API Tester proxy mode; to reproduce the page failure, use direct mode. Look up statuses anytime with HTTP Status Codes.
Implementer's note
On network-layer failures that look like CORS, the API Tester shows a hint and offers “enable server proxy and retry.” The proxy path hits same-origin /api/proxy and the server performs the upstream call, so the browser never preflights the target — similar to curl/Postman, and not a production frontend fix. An earlier revision of this article said the tester was “never subject to CORS,” which contradicted direct mode; the accurate split is direct constrained / proxy bypass. During the AdSense freeze we do not add new tool slugs; preflight-related workflows stay on the existing api-tester and http-status-codes tools.
Related reading
Provided by ToolVault. Related tools: API Tester, HTTP Status Codes, HTTP Method Cheatsheet, curl-to-code. See the homepage for more developer tools.
Related Tools
Related Articles
Incomplete HTTPS Certificate Chain: Browser Distrusts a “Valid” Leaf
Leaf dates and SAN look fine, but clients still reject the cert? Usually a missing intermediate or misordered fullchain. Verify Issuer→Subject with openssl s_client and a local decoder.
Complete Guide to HTTP API Testing: From Basics to Advanced Debugging Techniques
Master the core skills of HTTP API testing, including GET/POST/PUT/DELETE requests, header configuration, request body formats, authentication methods, and debugging techniques. Boost your development efficiency with an online API testing tool.
HTTP GET vs POST: When to Use Each? Semantics, Safety, and Best Practices
GET vs POST semantics, idempotency, caching, URL length limits, security tradeoffs, REST conventions, and common mistakes. Includes curl examples and a decision table for picking the right method.