How to Debug CORS Preflight Requests (OPTIONS) in Chrome
CORS preflight failures are uniquely frustrating. The error shows up on your actual request — a POST or PUT — but the failure happened 50ms earlier on a request you never wrote. Chrome fired an OPTIONS request automatically, the server responded incorrectly (or not at all), and now your request is dead before it started.
The preflight is the browser’s way of asking the server “is this allowed?” before committing to the real request. When the server answers wrong, you get a cryptic error message pointing at your payload request, not the preflight that actually failed.
This guide walks through finding the preflight in Chrome DevTools, reading the exact error, and using response header injection to isolate whether the preflight is your real problem.
Why Preflight Fires (and Why It’s Invisible)
Not every cross-origin request triggers a preflight. Simple requests — GET and POST with basic headers — go straight through. A preflight (OPTIONS request) fires when:
- The request method is
PUT,PATCH, orDELETE - The request includes custom headers:
Authorization,Content-Type: application/json,X-Request-ID, or anything not on the short safe-headers list - The request includes
credentials: 'include'
In practice: if your app is making authenticated API calls or sending JSON, preflight fires. That covers the vast majority of modern frontend-to-API patterns.
The reason it’s invisible: the browser sends the OPTIONS request automatically. Your JavaScript didn’t call it, your logs don’t show it, and the error message references the URL of your actual request — not the OPTIONS URL. Developers who haven’t hit this before often spend 20 minutes looking in the wrong place.
Step 1: Find the Preflight in Chrome DevTools
Open Chrome DevTools → Network tab. Reproduce the CORS error.
You’ll see two requests to the same URL:
- A request with method
OPTIONS— this is the preflight - Your actual request (which may be cancelled or show a CORS error)
If you don’t see the OPTIONS request, make sure the Network tab is not filtered to show only Fetch/XHR — the preflight appears separately and may require the “All” filter.
Click the OPTIONS request. You’re looking at two things:
Request headers (what the browser sent):
Access-Control-Request-Method: POST— tells the server what method the actual request will useAccess-Control-Request-Headers: authorization, content-type— lists the custom headers the actual request will send
Response headers (what the server sent back):
Access-Control-Allow-Origin— must match your origin (or be*)Access-Control-Allow-Methods— must include the actual request’s methodAccess-Control-Allow-Headers— must include all headers listed inAccess-Control-Request-Headers
If any of those response headers are missing or wrong, the preflight fails and your actual request never fires.
Step 2: Match the Error Message to the Problem
The console error tells you exactly what’s wrong — but you have to read the second half of it.
Missing origin:
Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin'
header is present on the requested resource.
The server’s OPTIONS response didn’t include Access-Control-Allow-Origin. CORS isn’t configured at all for this endpoint, or the OPTIONS method is hitting a different handler than expected.
Header not allowed:
Request header field 'authorization' is not allowed by Access-Control-Allow-Headers
in preflight response.
Your request sends Authorization, but the server’s preflight response doesn’t include it in Access-Control-Allow-Headers. The fix: add Authorization to your backend’s CORS allowed headers list.
Method not allowed:
Method PUT is not allowed by Access-Control-Allow-Methods in preflight response.
The server allows GET and POST but hasn’t added PUT to its CORS config.
Once you’ve matched the error, you know exactly what the server needs to return. Now comes the useful part.
Step 3: Use Response Header Injection to Simulate a Permissive Backend
Here’s the debugging move that saves time: instead of deploying a backend config change and re-testing, you can inject the correct CORS response headers directly in your browser using HeaderSnap. This confirms the preflight was the only problem — and tells you exactly what your backend needs to return.
Set up a response rule in HeaderSnap:
-
Open HeaderSnap → go to the Rules tab → create a new rule
-
Set URL pattern to match your API endpoint (e.g.,
https://api.yourapp.com/*) -
Set Header Target to Response
-
Each HeaderSnap rule sets one header — create three separate rules:
- Rule 1: header
Access-Control-Allow-Origin, valuehttp://localhost:3000 - Rule 2: header
Access-Control-Allow-Methods, valueGET, POST, PUT, DELETE, PATCH, OPTIONS - Rule 3: header
Access-Control-Allow-Headers, valueAuthorization, Content-Type, X-Request-ID
- Rule 1: header
-
Reload and retry the request.
If your request succeeds now, the preflight CORS config was the only issue. The server just needed to return those headers on the OPTIONS response.
This is a faster feedback loop than deploying backend changes — especially when you’re debugging against a staging API you don’t fully control, or trying to confirm whether a reported CORS issue is real before filing a ticket.
What this confirms:
- The browser is treating the injected headers as legitimate preflight approval
- Your actual request payload and auth flow work correctly
- The only fix needed is on the server side: add the correct origin, methods, and headers to its CORS config
What to Tell Your Backend Team
Once you’ve confirmed the issue, the backend fix is specific. You know exactly which headers and methods to allowlist because the Access-Control-Request-Headers field in the preflight told you.
// Express (Node.js) — cors package
app.use(cors({
origin: 'http://localhost:3000',
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
allowedHeaders: ['Authorization', 'Content-Type', 'X-Request-ID'],
}));
# FastAPI
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000"],
allow_methods=["*"],
allow_headers=["Authorization", "Content-Type", "X-Request-ID"],
)
The backend doesn’t need to guess — the Access-Control-Request-Headers field is the exact list. Match it, and the preflight passes.
Quick Reference: Preflight Debug Checklist
- Open DevTools → Network → All (not just Fetch/XHR)
- Find the OPTIONS request to the same URL as your failing request
- Check the OPTIONS response: does it include
Access-Control-Allow-Origin,-Methods, and-Headers? - Match the console error to the specific missing header
- Use HeaderSnap response rules to inject the correct CORS headers temporarily
- Confirm the actual request succeeds with injected headers
- File the exact fix with your backend team
Preflight failures look opaque from the outside, but the Chrome DevTools trace gives you everything you need: the exact OPTIONS request, the exact response (or lack of one), and the exact header that was missing. Once you can read that trace, debugging a preflight takes minutes.
The response header injection approach is what turns debugging into confirmation — you’re not guessing what the backend needs to return, you’re proving it.
Get HeaderSnap free →
Inject response headers in Chrome to debug CORS, CSP, and caching issues. No account required.
Found this useful? Share it with your team or check out the full guide to CORS debugging in local development.