Home JavaScript CORS Preflight Failed: Fix OPTIONS Response
Intermediate 6 min · September 23, 2026

CORS Preflight Failed: Fix OPTIONS Response

A failed CORS preflight means your OPTIONS reply lacked the right headers.

N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Drawn from code that ran under real load.

Follow
Production
production tested
September 23, 2026
last updated
1,905
articles · all by Naren
Before you start⏱ 12 min
  • Basic fetch API and HTTP methods
  • A backend you can configure
  • Browser DevTools Network familiarity
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • A preflight is an OPTIONS request the browser sends before your real call when you use JSON bodies, custom headers, or methods like PUT and DELETE.
  • Your server must answer OPTIONS with a 2xx status plus Access-Control-Allow-Methods and Access-Control-Allow-Headers, before any auth middleware runs.
  • A missing OPTIONS row in the Network tab means the main request failed instead. A red OPTIONS row means the preflight itself failed.
  • Keep preflights unauthenticated, echo validated origins, and test with an origin-carrying curl OPTIONS call after every change.
✦ Definition~90s read
What is CORS Preflight Response Fix?

A CORS preflight is an OPTIONS request the browser sends automatically before certain cross-origin calls to ask the server for permission details. The server never sees your JSON payload or custom headers in this probe. It sees three questions: the Origin making the call, the Access-Control-Request-Method naming the intended verb, and the Access-Control-Request-Headers listing the custom headers.

Imagine a courier who phones the front desk before driving over with a special delivery.

The browser sends your real request only when the answer satisfies every check.

Not every call gets preflighted. Simple requests skip the probe entirely: GET, HEAD, or POST using only safelisted headers and form-like content types such as text/plain or application/x-www-form-urlencoded. Everything else triggers one. POSTs with application/json bodies, which covers nearly every modern API call, any custom header like Authorization or X-Request-Id, and verbs like PUT, PATCH, and DELETE.

If your GETs pass and your JSON POSTs fail, a preflight is almost certainly the dividing line.

A correct preflight answer has three parts. A 2xx status, conventionally 204 with no body. An Access-Control-Allow-Methods line listing the verbs the endpoint accepts. And an Access-Control-Allow-Headers line listing every custom header the client may send.

With credentials, the answer also needs the echoed origin and Access-Control-Allow-Credentials true. Miss any part and the browser blocks the real request with a CORS error that names the failed check.

Three server mistakes cause most failures: no OPTIONS route (404/405), auth middleware 401ing a credential-free probe, or the CORS layer running after the handler returns a bare 200. Each breaks the handshake before app code runs, so preflight bugs belong to routing and middleware order.

Plain-English First

Imagine a courier who phones the front desk before driving over with a special delivery. The call states the package type and asks which entrance to use. If the desk answers with clear instructions, the courier drives over. If nobody picks up, the delivery never leaves the depot. The preflight is that phone call, your server is the front desk, and the browser is a strict courier who cancels the trip when the instructions are missing. Fix the front desk's answers and deliveries resume.

Your GET requests sail through, your POST with a JSON body dies, and the console blames CORS. The Network tab shows a lonely OPTIONS request glowing red while your real request never leaves the browser. Nothing is wrong with your fetch call. The browser asked your server for delivery instructions, got silence or a wrong answer, and cancelled the trip before starting it.

That cancelled trip is a failed preflight, and it deserves its own guide because its causes barely overlap with main-request CORS failures. A missing Allow-Origin on a GET is one fix. A 401-gated OPTIONS route, a framework that never registered an OPTIONS handler, or an Nginx block that proxies OPTIONS upstream instead of answering it are entirely different bugs with different owners.

This guide focuses on the preflight exchange only. You will learn which requests trigger one, how to tell a preflight failure from a main-request failure in the Network tab, the exact headers and status a correct OPTIONS answer carries, and how to wire that answer in Express, Spring, and Nginx. Pair it with the general CORS policy article for main-request failures.

What Triggers a Preflight: The Three Rules That Matter

Browsers preflight a cross-origin request when it is not simple, and simple has a strict definition. The method must be GET, HEAD, or POST. The headers must stay within the safelist: Accept, Accept-Language, Content-Language, Content-Type, and Range with limits. And the Content-Type must be a form-like value: application/x-www-form-urlencoded, multipart/form-data, or text/plain. Step outside any of the three and the browser sends an OPTIONS probe first.

In practice, three triggers cover nearly every failure. A JSON body sets Content-Type to application/json, which is outside the safelist, so every JSON POST is preflighted. Custom headers such as Authorization, X-Request-Id, or X-CSRF-Token break the header rule even on GET requests. And the verbs PUT, PATCH, and DELETE break the method rule regardless of headers. If your GETs pass and everything else fails, you are staring at one of these three.

This selectivity explains the confusing partial outages. A dashboard that reads with GET keeps working while its save buttons, which PUT JSON, all die. A public endpoint without auth passes while the authenticated one fails, because only the second sends Authorization. The pattern of what works is the diagnosis: find what the failing calls share and you have named your trigger.

The snippet below shows the most common trigger in the wild, a JSON POST, alongside a simple form POST that skips the probe. Run them against any test API with the Network tab open and watch one produce an OPTIONS row while the other does not.

preflight-triggers.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
async function jsonPost(url) {
  const res = await fetch(url, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ name: 'lamp', qty: 2 })
  });
  console.log('json post status:', res.status);
  return res;
}

async function formPost(url) {
  const body = new URLSearchParams({ name: 'lamp', qty: '2' });
  const res = await fetch(url, { method: 'POST', body });
  console.log('form post status:', res.status);
  return res;
}
Try it live
📊 Production Insight
Adding a single tracing header like X-Request-Id to all outbound calls silently converts every simple request into a preflighted one. Teams have taken down browser clients with an observability change that looked harmless. When you add a global header, update the server's Allow-Headers the same day.
🎯 Key Takeaway
JSON bodies, custom headers, and PUT/PATCH/DELETE verbs each trigger a preflight. When some calls pass and others fail, the failing group shares one of these three triggers.

Reading the OPTIONS Exchange in the Network Tab

The Network tab tells the whole preflight story in two rows. The first row is the OPTIONS probe carrying Origin, Access-Control-Request-Method, and Access-Control-Request-Headers. The second row, present only when the probe passes, is your real request. A failed preflight shows the OPTIONS row in red and no second row at all, because the browser cancelled the trip. That missing second row is the single most reliable signal you are debugging a preflight and not a main-request failure.

Click the OPTIONS row and read three things in order. The status code: anything outside 2xx fails the probe, and 401 or 405 each point at a specific layer. The Response Headers: Allow-Methods and Allow-Headers must be present with values covering your call. And the timing: a 2 ms rejection means edge middleware killed it, while a full round trip suggests the app itself answered badly.

Request headers deserve equal attention. Access-Control-Request-Headers lists exactly what the browser asked to send, so compare it token by token against the Allow-Headers reply. Developers often spot the gap here in seconds: the client sends authorization plus X-Request-Id while the server allows only content-type. The console message names the rejected header too, but the two header blocks side by side remove all doubt.

Finish by checking the console message against what you found. A disallowed-method message with a 204 OPTIONS means Allow-Methods is short. A disallowed-header message with 200 means Allow-Headers is short. A redirect or 401 on OPTIONS means routing or auth. Each pairing maps to one fix in a later section.

preflight-diagnose.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
async function diagnosePreflight(url, headers) {
  try {
    const res = await fetch(url, { method: 'POST', headers });
    console.log('real request sent, status:', res.status);
    console.log('open Network tab: expect an OPTIONS row above this call');
  } catch (err) {
    console.error('blocked:', err.message);
    console.error('check the OPTIONS row status and Allow-* headers');
  }
}

diagnosePreflight('https://api.example.com/items', {
  'Content-Type': 'application/json',
  'X-Request-Id': 'abc-123'
});
Try it live
📊 Production Insight
Support teams cut CORS triage time sharply when taught the two-row rule: red OPTIONS plus no second row means preflight, full stop. That one sentence keeps preflight tickets out of the application queue and routes them to whoever owns middleware order.
🎯 Key Takeaway
A red OPTIONS row with no following real request means the preflight failed. Read its status and Allow headers first, then match the console message to the gap.

The Four Headers Your OPTIONS Answer Must Return

A correct preflight answer is small and exact. The status is 2xx, with 204 No Content as the convention since there is no body to return. Access-Control-Allow-Origin echoes the requesting origin or names one allowed origin. Access-Control-Allow-Methods lists the verbs the endpoint accepts, such as GET, POST, PUT, DELETE, OPTIONS. Access-Control-Allow-Headers lists every custom header the client may send. With credentials in play, Access-Control-Allow-Credentials true joins them, plus Vary: Origin so caches keep per-origin answers apart.

Each missing piece fails differently. No Allow-Methods fails PUT and DELETE while GET passes. No Allow-Headers fails every call carrying the unlisted header, with the console naming it. A wildcard origin fails credentialed calls outright. The browser checks all of them against the probe's request headers, so partial answers produce partial outages that look random until you line up the headers.

Access-Control-Max-Age deserves a deliberate value. It tells the browser how many seconds to cache the probe result, turning one OPTIONS exchange into minutes of direct calls. A value like 600 keeps chatty apps fast. During development, though, the cache hides your fixes, so test with DevTools cache disabled or you will conclude a working fix is broken.

The snippet below is a dependency-free Node server that answers OPTIONS correctly and echoes a validated origin. It runs with plain node server.js and shows the minimal shape every framework config must reproduce: early OPTIONS return, 204 status, and the four headers before any other logic.

options-server.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const http = require('http');
const allowed = new Set(['https://app.example.com']);

const server = http.createServer((req, res) => {
  const origin = req.headers.origin;
  if (origin && allowed.has(origin)) {
    res.setHeader('Access-Control-Allow-Origin', origin);
    res.setHeader('Vary', 'Origin');
  }
  if (req.method === 'OPTIONS') {
    res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
    res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
    res.setHeader('Access-Control-Max-Age', '600');
    res.writeHead(204);
    res.end();
    return;
  }
  res.setHeader('Content-Type', 'application/json');
  res.end(JSON.stringify([{ id: 1 }]));
});

server.listen(3000, () => console.log('api on :3000'));
Try it live
⚠ Answer OPTIONS Before Everything Else
The OPTIONS handler must run before auth, body parsing, and rate limiting. Any layer that rejects the probe first turns its own error into a CORS failure the browser will blame on headers.
📊 Production Insight
The always-on-error variant of this bug hides in plain sight: OPTIONS passes on happy paths but the gateway strips CORS headers on 4xx answers. Probe an error path, not just the healthy one, or your first real outage will wear a CORS disguise.
🎯 Key Takeaway
Answer OPTIONS with 204 plus echoed origin, Allow-Methods, and Allow-Headers before any other middleware. Cache the result with Max-Age and disable cache while testing fixes.

Preflight Failure vs Main-Request Failure: Telling Them Apart

These two failures print similar console text but need opposite fixes, so telling them apart is the highest-value skill in this guide. A preflight failure shows a red OPTIONS row and no real request row. The fix lives in routing and middleware order. A main-request failure shows the real GET or POST row with a response that lacks Allow-Origin. The fix lives in response headers. Mixing them up sends you editing response headers for a request that never left.

Status codes sharpen the split. On the OPTIONS row, 401 means auth gated the probe, 404 or 405 means no OPTIONS route exists, and 3xx means a redirect swallowed the probe. On the real request row, any status with a body but no Allow-Origin means the endpoint works and only permission headers are missing. The row the status sits on names the layer to fix.

Curl reproduces each side cleanly. An OPTIONS curl with Access-Control-Request-Method replays the probe from any terminal, which matters when the failing browser belongs to a customer. A plain GET curl with an Origin header replays the main request. Run both against the failing URL and you will know within a minute which half is broken, without screen-sharing.

Beware the hybrid that fools veterans: a preflight passes, then the real request fails its own check. The Network tab shows a green OPTIONS row followed by a red POST. That is a main-request failure wearing preflight clothes. Confirm by reading the POST's response headers for Allow-Origin before touching the OPTIONS config.

which-half-failed.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
async function whichHalfFailed(url) {
  try {
    const res = await fetch(url, {
      method: 'PUT',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ qty: 3 })
    });
    console.log('main request sent, status:', res.status);
    console.log('if this logged, the preflight passed; check response headers');
  } catch (err) {
    console.error('blocked before or during send:', err.message);
    console.error('red OPTIONS row = preflight failed; red PUT row = main failed');
  }
}

whichHalfFailed('https://api.example.com/items/1');
Try it live
📊 Production Insight
Incident reviews show engineers spending the first 20 minutes on the wrong half because both print CORS text. Put the two-row rule in the runbook's first line and that wasted window collapses to a glance at the Network tab.
🎯 Key Takeaway
Red OPTIONS with no real request means preflight failure. Green OPTIONS plus red real request means main-request failure. Fix routing for the first and response headers for the second.

Framework Fixes Without Repeating the Basics

Every stack below does the same three things: answer OPTIONS with 204, emit the Allow headers, and run that logic before auth. What differs is where the code lives. In Express, mount a preflight handler before all other middleware, including auth and JSON parsing. The cors package does this when mounted first, but a hand-rolled five-line middleware works identically and keeps the ordering visible in your own code.

In Spring, declare the CORS mapping once in a WebMvcConfigurer with explicit origins, methods, and headers, and let the framework's built-in OPTIONS handling answer probes. Controller-level annotations work for single endpoints but scatter the config, so prefer the global mapping. Verify that Spring Security permits OPTIONS requests without authentication, since the security chain sits in front of MVC handling and can 401 probes before they reach it.

In Nginx, answer OPTIONS directly in the location block with a 204 and the Allow lines, then return before proxying upstream. Use the always flag on every add_header so error responses keep their CORS headers too. Without always, a healthy probe on 200 paths hides the fact that every 4xx probe leaves headerless, and your first rate-limit event becomes a fake CORS outage.

The ordering rule survives every framework: CORS before auth, OPTIONS before proxying, explicit lists instead of wildcards with credentials. Audit the chain after each dependency upgrade, since middleware registration order is exactly the kind of thing a major version quietly reshuffles.

express-preflight.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const express = require('express');
const app = express();
const allowed = new Set(['https://app.example.com']);

app.use((req, res, next) => {
  if (allowed.has(req.headers.origin)) {
    res.setHeader('Access-Control-Allow-Origin', req.headers.origin);
    res.setHeader('Vary', 'Origin');
  }
  if (req.method === 'OPTIONS') {
    res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
    res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
    return res.status(204).end();
  }
  next();
});

app.use(express.json());
app.get('/items', (req, res) => res.json([{ id: 1 }]));
app.listen(3000, () => console.log('api on :3000'));
Try it live
📊 Production Insight
Framework upgrades are the quiet killer here. A minor Express or Spring release rarely touches CORS, but major versions have reordered default chains. After every major bump, run one unauthenticated OPTIONS probe per critical endpoint before declaring victory.
🎯 Key Takeaway
Mount preflight handling before auth in Express, use a global WebMvcConfigurer plus security exemptions in Spring, and answer OPTIONS with 204 and always-flagged headers in Nginx.

Caching, Credentials, and the Last Three Gotchas

Three details cause the failures that survive a correct initial setup. First, preflight caching. Browsers cache successful probes for Access-Control-Max-Age seconds, so a fixed server can look broken until the old negative result expires. During development, disable cache in DevTools. In production, pick a Max-Age like 600 that balances chattiness against how fast you want config changes to propagate.

Second, credentials. The probe itself never carries cookies or Authorization, but the real request might, and the OPTIONS answer must already promise credential support with an echoed origin plus Allow-Credentials true. Teams often configure credentials on the main handler and forget the probe, so anonymous calls pass and logged-in calls fail on the identical endpoint. Configure both halves together.

Third, redirects. If the probed URL redirects, the browser applies the CORS check to the final response, and most servers do not emit Allow headers on redirect hops. Point cross-origin clients at final URLs and probe the exact URL the client calls, not its unredirected parent. A probe that passes on the bare domain and fails on the www subdomain is this bug wearing a costume.

Close the loop with a deploy-time probe. One scripted OPTIONS call per critical endpoint, asserting 204 plus the Allow headers, catches reorders, upgrade reshuffles, and missing headers before users do. It runs in under a second and has saved more launches than any dashboard.

💡Probe the Exact Client URL
Redirects, trailing slashes, and subdomain differences each change which server block answers OPTIONS. Always test the precise URL your client calls, not a nearby one that happens to work.
📊 Production Insight
Stale preflight caches turn fixed bugs into zombie tickets: the server is correct, one engineer's browser still fails, and the ticket reopens. A cache-disabled retest takes ten seconds and closes the loop honestly.
🎯 Key Takeaway
Cache probes with Max-Age but test with cache disabled, promise credentials on the OPTIONS answer as well as the main one, and probe the exact client URL including redirects.
● Production incidentPOST-MORTEMseverity: high

Auth Middleware 401'd Every Preflight for 53 Minutes

Symptom
At 10:05 AM, the web app's error rate jumped from 0.4 percent to 61 percent across all JSON endpoints, while the native mobile apps showed zero change. Browser consoles filled with preflight failures on OPTIONS, and the real POST and PUT requests never appeared in the Network tab. Backend CPU and latency stayed flat because the failing probes were rejected in 2 ms by the auth layer before reaching any handler.
Assumption
The security team had reordered middleware so authentication ran before all other processing, assuming earlier auth meant a smaller attack surface. Staging tests passed because the staging suite called the API from same-origin test pages and with plain curl, neither of which sends preflights. Nobody in review noticed that preflights carry no Authorization header by design.
Root cause
The new order ran JWT validation before the CORS middleware, so every OPTIONS probe without credentials received a 401 in about 2 ms. Browsers require a 2xx preflight answer with Allow-Methods and Allow-Headers, so all 41 preflighted endpoints in the web app went dark at once. Mobile apps were unaffected because they send no Origin header and never trigger preflights. The blast radius was the entire browser client within 4 minutes of deploy.
Fix
The team restored CORS handling ahead of auth at 10:58 AM and added an explicit early return for OPTIONS with a 204 plus the Allow headers. They then pinned the middleware order in a startup test that boots the app and asserts an unauthenticated OPTIONS call returns 204. The API test suite gained a preflighted cross-origin case for each of the 41 affected endpoints, and staging now runs one browser-driven smoke test that performs a real JSON POST.
Key lesson
  • Middleware order is a security-relevant decision, so any reorder needs a preflight test. An unauthenticated OPTIONS probe returning 204 is a five-line test that would have blocked this deploy.
  • Same-origin and curl tests never send preflights, which makes them blind to this whole failure class. At least one test must issue a real cross-origin JSON call.
  • Mobile apps surviving while browsers die is the signature of a preflight bug. Teach on-call that split so triage starts at the middleware chain instead of the database.
Production debug guideSix steps that separate preflight failures from main-request failures and land on the broken layer.6 entries
Symptom · 01
Console shows a CORS error on a JSON POST, PUT, PATCH, or DELETE
Fix
Open DevTools Network and look one row above the failed call for the OPTIONS request. If a red OPTIONS row exists, the preflight failed and your real request never left. If no OPTIONS row exists, the request was simple and this guide does not apply, so debug the main response headers instead.
Symptom · 02
A red OPTIONS row exists in the Network tab
Fix
Click it and read the status plus Response Headers. A 401 means auth middleware rejected the probe, so exempt OPTIONS from auth. A 404 or 405 means no route handles OPTIONS, so register one. A 200 without Allow-Methods or Allow-Headers means the CORS layer ran too late in the chain.
Symptom · 03
You need to reproduce without a browser
Fix
Send the probe by hand: curl -X OPTIONS -H 'Origin: https://app.example.com' -H 'Access-Control-Request-Method: POST' -H 'Access-Control-Request-Headers: content-type,authorization' -v https://api.example.com/items. Expect a 2xx with Allow-Methods and Allow-Headers in the reply. Anything else is your bug, reproducible from any terminal.
Symptom · 04
Preflight returns 200 but the browser still blocks
Fix
Compare the Allow-Headers value against the exact headers your code sends, including casing and every X- prefixed name. One missing header fails the whole check. Add it server-side, then retest with DevTools cache disabled so a stale preflight result does not fool you.
Symptom · 05
Credentialed calls fail while anonymous calls pass
Fix
Confirm the OPTIONS answer echoes your exact origin and carries Access-Control-Allow-Credentials: true. A wildcard origin is rejected with credentials. Fix the echo logic, add Vary: Origin, and keep the preflight itself credential-free.
Symptom · 06
Preflight passes directly but fails through the CDN or gateway
Fix
Replay the curl OPTIONS probe against each hop: origin server first, then the gateway URL. The hop where Allow headers vanish is stripping or caching them. Forward the CORS headers at that layer and add Vary: Origin so cached probes do not leak across origins.
Preflight Failure Causes Compared
Root CauseHow to ConfirmFixPrevention
Auth middleware rejects OPTIONS with 401OPTIONS row shows 401 in ~2 msExempt OPTIONS from auth and answer 204 firstStartup test asserts unauthenticated OPTIONS returns 204
No route handles OPTIONSOPTIONS row shows 404 or 405Register an OPTIONS handler returning Allow headersProbe every endpoint with OPTIONS in integration tests
CORS layer runs after the handlerOPTIONS returns 200 with no Allow headersMove CORS middleware before auth and parsingAudit middleware order after each upgrade
Allow-Headers missing a custom headerConsole names the exact rejected headerAdd the header to Allow-Headers verbatimLog probe decisions so new headers get allowlisted
Gateway strips probe headersDirect origin probe passes but gateway probe failsForward Allow headers and add Vary: OriginRun header checks through the full proxy chain
⚙ Quick Reference
5 commands from this guide
FileCommand / CodePurpose
preflight-triggers.jsasync function jsonPost(url) {What Triggers a Preflight
preflight-diagnose.jsasync function diagnosePreflight(url, headers) {Reading the OPTIONS Exchange in the Network Tab
options-server.jsconst http = require('http');The Four Headers Your OPTIONS Answer Must Return
which-half-failed.jsasync function whichHalfFailed(url) {Preflight Failure vs Main-Request Failure
express-preflight.jsconst express = require('express');Framework Fixes Without Repeating the Basics

Key takeaways

1
JSON bodies, custom headers, and PUT/PATCH/DELETE verbs trigger an OPTIONS preflight.
2
The server must answer OPTIONS with 2xx plus Allow-Methods and Allow-Headers.
3
Preflight handling must run before auth, parsing, and proxying.
4
A red OPTIONS row with no real request means the preflight failed; fix routing, not response headers.
5
Reproduce with origin-carrying OPTIONS curl and retest with cache disabled.
6
Promise credentials on the probe answer too, and probe the exact client URL.

Common mistakes to avoid

5 patterns
×

Protecting OPTIONS with authentication

Symptom
GET works but JSON POST fails with a 401 preflight and the real request never leaves.
Fix
Return 204 for OPTIONS before the auth layer, since probes carry no credentials by design.
×

Returning 200 without Allow headers on OPTIONS

Symptom
The probe succeeds at the HTTP level yet the browser still blocks with a CORS error.
Fix
Include Access-Control-Allow-Methods and Access-Control-Allow-Headers on every OPTIONS answer.
×

Debugging with plain curl and no Origin header

Symptom
curl returns 200 while every browser fails, so the team declares the server innocent.
Fix
Replay the probe with Origin plus Access-Control-Request-Method and read the Allow headers.
×

Adding a global custom header without updating Allow-Headers

Symptom
All calls fail at once after an observability or tracing change that looked harmless.
Fix
List every client-sent header in Allow-Headers on the same day the header ships.
×

Testing fixes with the preflight cache warm

Symptom
A correct server still looks broken until the old cached probe expires.
Fix
Retest with DevTools cache disabled or in a fresh profile after every server change.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
Which requests trigger a CORS preflight and why?
Q02JUNIOR
What must a correct OPTIONS response contain?
Q03SENIOR
Why must preflights skip authentication?
Q04SENIOR
How do you tell a preflight failure from a main-request failure?
Q05SENIOR
Preflights pass directly but fail through the CDN. What is happening?
Q01 of 05JUNIOR

Which requests trigger a CORS preflight and why?

ANSWER
Requests that are not simple: methods beyond GET, HEAD, or POST, custom headers like Authorization, or non-form content types like application/json. The browser probes with OPTIONS first so the server can approve the method and headers before any real data travels.
FAQ · 6 QUESTIONS

Frequently Asked Questions

01
Do simple GET requests ever send a preflight?
02
Why does the real request never appear in the Network tab?
03
Can I disable preflights from the client?
04
How long is a preflight result cached?
05
Should OPTIONS requests hit my rate limiter?
06
Why do mobile apps work while browsers fail?
N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Drawn from code that ran under real load.

Follow
Verified
production tested
September 23, 2026
last updated
1,905
articles · all by Naren
🔥

That's Browser. Mark it forged?

6 min read · try the examples if you haven't

Previous
ReferenceError Not Defined Fix
1 / 3 · Browser
Next
EADDRINUSE Port Already in Use Fix