When a cross-origin request is "complex" (uses methods other than GET/HEAD/POST,
has custom headers, or uses certain Content-Types), the browser automatically sends
a preflight OPTIONS request first.
The preflight checks if the actual request is allowed. Only if the server responds
with the right CORS headers will the browser send the real request.
Preflight Flow
1
Browser detects complex request
Your code calls fetch() with custom headers or non-simple method
fetch('https://api.example.com/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Custom-Header': 'value'
}
});
↓
2
Browser sends OPTIONS request
Asks the server what's allowed
OPTIONS /data HTTP/1.1
api.example.com
https://myapp.com
POST
content-type, x-custom-header
↓
3
Server responds with permissions
Lists allowed origins, methods, and headers
HTTP/1.1 204 No Content
https://myapp.com
GET, POST, PUT, DELETE
Content-Type, X-Custom-Header
86400
↓
4
Browser sends actual request
If preflight passed, the real request is sent
POST /data HTTP/1.1
api.example.com
https://myapp.com
application/json
value
{"data": "payload"}
What Triggers Preflight?
fetch(url, { method: 'PUT' });
fetch(url, { method: 'DELETE' });
fetch(url, { method: 'PATCH' });
fetch(url, {
headers: { 'X-API-Key': 'abc' }
});
fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
fetch(url);
fetch(url, { method: 'POST' });
fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'text/plain' },
body: 'text data'
});
Simple Content-Types (no preflight):
text/plain
application/x-www-form-urlencoded
multipart/form-data
Server-Side: Handling Preflight (Node.js)
import { createServer } from 'node:http';
const server = createServer((req, res) => {
res.setHeader('Access-Control-Allow-Origin', 'https://myapp.com');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
if (req.method === 'OPTIONS') {
res.setHeader('Access-Control-Max-Age', '86400');
res.writeHead(204);
res.end();
return;
}
if (req.method === 'POST' && req.url === '/api/data') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: true }));
}
});
server.listen(3000);
Access-Control-Max-Age tells the browser how long to cache the preflight
response. This avoids sending OPTIONS before every request.
Common Preflight Errors
fetch(url, { method: 'DELETE' });
fetch(url, {
headers: { 'X-Custom': 'value' }
});
When preflight fails, you'll see a CORS error in the console. The actual
request is never sent — the browser blocks it at the preflight stage.
Debugging Preflight
In browser DevTools Network tab:
- Look for OPTIONS requests (they appear before your actual request)
- Check the response headers for
Access-Control-Allow-*
- If preflight fails, the actual request won't appear
- The Console will show CORS error messages
← Back to HTTP Examples