How HTTP Works

Glossary Term

Preflight Request (CORS OPTIONS)

A CORS preflight is an automatic OPTIONS request the browser sends before a cross-origin request to check permission. See when it fires and how to answer it.

Reviewed 2 min readintermediate2 sourcesMarkdown
On this page

TL;DR: A preflight is an OPTIONS request the browser sends by itself before a cross-origin request that could have side effects, to ask the server whether it allows that method and those headers.

A CORS preflight request is an automatic OPTIONS request that a browser sends to a cross-origin URL before the actual request, when that request is not a “simple” one. The server’s answer tells the browser whether to send the real request at all. Your JavaScript never sees the preflight and cannot add headers to it.

What triggers one

You get a preflight when a cross-origin request uses a method other than GET, HEAD or POST, or sets a header outside the CORS-safelisted set (Accept, Accept-Language, Content-Language, Content-Type limited to three values, and Range in simple form). The two triggers people hit most are:

  • Authorization header on any request
  • Content-Type: application/json, because only application/x-www-form-urlencoded, multipart/form-data and text/plain are safelisted

The exchange

OPTIONS /v1/orders HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: authorization, content-type
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, PUT, DELETE
Access-Control-Allow-Headers: authorization, content-type
Access-Control-Max-Age: 600
Vary: Origin

If the answer does not cover the method and headers, Chrome logs Response to preflight request doesn't pass access control check and the real request is never sent.

Non-obvious facts

  • The preflight must succeed with an ok status. A 401, 403, 404 or 500 on the OPTIONS request fails CORS even if your PUT handler is fine. Auth middleware that runs first is the usual culprit, because the preflight carries no cookies or Authorization header.
  • Access-Control-Max-Age is capped by the browser. The default is 5 seconds, so without it every cross-origin call becomes two round trips. Chromium caps the value at 2 hours (7200) and Firefox at 24 hours.
  • Add Vary: Origin when you echo the origin. Otherwise a CDN may cache one origin’s preflight and serve it to another.
  • Credentials need exact values. With Access-Control-Allow-Credentials: true, the wildcard * is not accepted for origin, headers or methods.
  • Redirects on a preflight fail. The preflight response must not be a redirect, so a trailing-slash 301 on the API path breaks it.

Go deeper

Frequently asked questions

What is a CORS preflight request?

It is an OPTIONS request the browser sends on its own, before the real request, to ask the target server whether the cross-origin method and headers are allowed.

When does the browser send a preflight?

When the request is not a simple request: a method other than GET, HEAD or POST, a non-safelisted header such as Authorization or a JSON Content-Type, or a few other conditions defined by the Fetch Standard.

Why does my preflight fail with a 401 or 404?

Preflights never carry credentials, so authentication middleware that runs before your CORS handler rejects them. Answer OPTIONS before auth, with a 2xx status.

How long is a preflight response cached?

As long as Access-Control-Max-Age says, capped by the browser. The default is 5 seconds, and Chromium caps it at 2 hours while Firefox allows up to 24 hours.

Sources

  1. MDN Web Docs: Preflight requestdeveloper.mozilla.org
  2. WHATWG Fetch Standard: CORS-preflight requestfetch.spec.whatwg.org

Keep going

Browse /search