Skip to content

/glossary/cors/preflight-request

What is a CORS preflight request?

verified onenvironmentlocal · windows 10.0.26200 arm64 · GNU bash 5.3.15 (Git Bash)updated
tool
CORS
versions
Fetch Standard
os
any

A CORS preflight request is an automatic OPTIONS request that a browser sends to another origin before the real request, when that request is not "simple": a method other than GET, HEAD or POST, a custom header such as Authorization, or a Content-Type like application/json. It carries Origin, Access-Control-Request-Method and Access-Control-Request-Headers; the server must answer with matching Access-Control-Allow-* headers and a 2xx status, or the browser blocks the real request.

What the exchange looks like

A server on port 4103 that answers preflights, and curl sending the same OPTIONS request a browser would send for a PUT with a JSON body:

bash
cat > server.js <<'EOF'
require("http").createServer((req, res) => {
  if (req.method === "OPTIONS") {
    res.writeHead(204, {
      "Access-Control-Allow-Origin": "https://app.example",
      "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE",
      "Access-Control-Allow-Headers": "Content-Type, Authorization",
      "Access-Control-Max-Age": "600",
    });
    return res.end();
  }
  res.setHeader("Access-Control-Allow-Origin", "https://app.example");
  res.end("real response\n");
}).listen(4103, "127.0.0.1");
setTimeout(() => process.exit(0), 5000);
EOF
node server.js &
sleep 1
curl -sS -i -X OPTIONS http://127.0.0.1:4103/items/1 \
  -H "Origin: https://app.example" \
  -H "Access-Control-Request-Method: PUT" \
  -H "Access-Control-Request-Headers: content-type" | grep -v -i '^date:'

Output

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 600
Connection: keep-alive
Keep-Alive: timeout=5

The browser compares the answer with the request it wants to make. Here PUT and content-type are allowed for https://app.example, so it proceeds with the real PUT. Access-Control-Max-Age: 600 lets it cache that answer for 10 minutes.

When a preflight is sent

RequestPreflight?
GET or HEAD without custom headersNo
POST with Content-Type text/plain, multipart/form-data or application/x-www-form-urlencodedNo
POST with Content-Type application/jsonYes
PUT, PATCH or DELETEYes
Any request with Authorization or another custom headerYes
Same-origin requestNever (CORS does not apply)

Common preflight failures

  • The server or a framework route does not handle OPTIONS and returns 404 or 405.
  • Authentication middleware rejects the preflight with 401: preflights never carry credentials, so OPTIONS must be allowed without them.
  • Access-Control-Allow-Headers does not list a header the request uses (often Authorization).
  • With credentials: "include", Access-Control-Allow-Origin: * is not accepted; the server must echo the exact origin and send Access-Control-Allow-Credentials: true.

Sources