Marouane Souda
What Are Preflight Requests and Why They Matter

Updated on

What Are Preflight Requests and Why They Matter

If you've been reading about Cross-Origin Resource Sharing, or CORS in short, there is a good chance that you've come across this term: Preflight Request. It's an extra HTTP request that the browser makes before the actual, cross-origin request to make sure it's safe to send.

It's basically an HTTP OPTIONS request that is automatically sent by browsers before certain cross-origin requests. It checks with the web server, and only if the server responds with the right CORS permissions does the browser proceed with the real request, otherwise the browser blocks it.

This makes CORS secure: a browser checks both origin and intent before sending your actual data.

How do Preflight Requests work?

Let’s break it down with a real example:

A preflight request from the browser to the server

OPTIONS /resource HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Authorization, Content-Type

As you can see, there are three essential headers that must be present in a preflight request:

  • Origin: is the domain that the request will originate from.
  • Access-Control-Request-Method: specifies the HTTP method of the subsequent request (POST in our case).
  • Access-Control-Request-Headers: lists the HTTP headers that will be used for the actual request.

A preflight response from the server to the browser

First, this all would be meaningless if the server is not configured to support cross-origin requests, because the Same-Origin Policy would be triggered and the intended request will be denied. So, the server must be configured with CORS.

Then, the server will send a response to the preflight request containing these headers:

  • Access-Control-Allow-Origin: lists the allowed origins. It can be a specific domain, or a wildcard *, which means all origins are allowed (not recommended).
  • Access-Control-Allow-Methods: specifies the HTTP methods that are permitted.
  • Access-Control-Allow-Headers: indicates the headers that can be used when making the intended request.

Here is a sample response to a preflight request:

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: HEAD, GET, POST, OPTIONS, PUT, PATCH, DELETE
Access-Control-Allow-Headers: Authorization, Content-Type

The browser sees the response, and since it checks out, it is successful, and the browser continues with the actual request. Otherwise it will block it and show an appropriate CORS error message.

When Do Preflight Requests Happen?

Preflight requests are not always needed. In fact, simple requests do not need a preflight request check.

By "simple requests", I mean requests with:

  • Allowed HTTP methods only: GET, HEAD, POST.
  • No custom headers, such as Authorization, X-Custom-Header, or Accept-Encoding. Only Accept, Accept-Language, Content-Language, and Content-Type (with the allowed values).
  • A Content-Type header value of text/plain, multipart/form-data, application/x-www-form-urlencoded.
  • No credentials: "include" in the request (if present, it triggers preflight).

Any violation of these conditions and the browser will consider the request as a "complex", thus triggering a preflight request. For example, using application/json as the value for the Content-Type header.

How Will the Browser Enforce CORS Policies For Simple Requests

Even though the browser sends the request, it still enforces CORS when it receives the response. So, if the response does not contain a valid Access-Control-Allow-Origin header, either because the server is not configured with CORS, or doesn't permit our origin from making the request, the browser will block JavaScript from accessing the response body.

How to Fix Preflight Issues

You can fix Preflight issues by setting up a OPTIONS handler. You can do this in two ways:

First method: ensure all your APIs respond to OPTIONS with the correct Access-Control-Allow-* header

Here is an example using Express.js.

app.options('/resource', (req, res) => {
  res.set({
    'Access-Control-Allow-Origin': 'https://app.example.com',
    'Access-Control-Allow-Methods': 'GET,POST,OPTIONS',
    'Access-Control-Allow-Headers': 'Content-Type,Authorization',
  });
  res.sendStatus(204);
});

Second method (preferred): use CORS middleware, and let the framework handle it

app.use(cors({
  origin: 'https://app.example.com',
  methods: ['GET','POST','OPTIONS'],
  allowedHeaders: ['Content-Type','Authorization'],
}));

More Tips

Include Access-Control-Allow-Credentials for Credentialed Requests

If your request uses credentials: 'include' to send cookies or authentication information, the server must respond with Access-Control-Allow-Credentials: true. Without it, the browser will block access to the response data, even if the request itself succeeds.

Also, the server cannot use a wildcard (*) for Access-Control-Allow-Origin; it must specify the exact origin to avoid exposing sensitive data to unauthorized sites.

Cache the Preflight Request

Add a Access-Control-Max-Age header to cache the preflight request. That way, you won't have to send another preflight request.

This header takes a numeric value of seconds, and its default value is 5 seconds.