Why CORS exists

The browser's same-origin policy prevents a page from reading resources that come from a different origin. That block is what stops a malicious site from silently pulling in data from another site the user is logged into. The side effect is that it also blocks legitimate cross-origin requests, such as fetching JSON from an API on another domain or drawing an image from a third-party host into a <canvas> element. Historically, developers resorted to workarounds like JSONP to get around this. Cross-Origin Resource Sharing (CORS) provides a formal mechanism for a server to state that a resource is safe for another origin to use.

How HTTP requests are structured

Browsers and servers exchange data over the network using the Hypertext Transfer Protocol (HTTP), which defines the rules for that communication. Every request and response message has two parts that determine what data flows and how it is treated.

The header holds metadata about the message, expressed as key-value pairs. The request header carries information like the expected response format or cookies the client already holds. The response header conveys details such as the encoding of the body or instructions not to cache the result.

The body carries the content itself, which can be plain text, image binary, JSON, HTML, or any other format.

Sample request header

Accept: text/html
Cookie: Version=1

Sample response header

Content-Encoding: gzip
Cache-Control: no-store

The CORS handshake

CORS works by having the server explicitly grant permission to a specific requesting origin. The browser does not lift the same-origin restriction on its own; it only shares the response data if the server provides the right signal.

  1. Client request: When the browser makes a cross-origin request, it includes an Origin header identifying the requesting origin (scheme, host, and port).
  2. Server response: If the server wants to allow access, it echoes that origin back in an Access-Control-Allow-Origin header, or uses * to permit any origin.
  3. Browser delivery: Only when the response contains a matching Access-Control-Allow-Origin header does the browser expose the response data to the requesting site.

Sending credentials with CORS

By default, CORS traffic is meant to be anonymous, so the requester's identity stays unknown. If you need to send cookies, which identify the user, the request and response need extra headers.

On the request side, add credentials: 'include' to the fetch options so the browser attaches the cookie to the request.

fetch('https://example.com', {
  mode: 'cors',
  credentials: 'include'
})

The server response must then set Access-Control-Allow-Credentials to true and Access-Control-Allow-Origin to a concrete origin. A wildcard * is not allowed here, because the server needs to know exactly who is being credentialed.

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Credentials: true

Preflighting complex requests

The CORS specification considers a request "complex" if it goes beyond the basics: it uses a method other than GET, POST, or HEAD; it includes headers beyond Accept, Accept-Language, or Content-Language; or it sends a Content-Type other than application/x-www-form-urlencoded, multipart/form-data, or text/plain.

For such calls, the browser automatically sends a so-called preflight request before the real request. This is an OPTIONS message that asks the server which operations it is willing to accept from this origin.

OPTIONS /data HTTP/1.1
Origin: https://example.com
Access-Control-Request-Method: DELETE

The server answers the preflight with the methods it permits for that origin:

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Methods: GET, DELETE, HEAD, OPTIONS

The preflight response may also carry an Access-Control-Max-Age header that tells the browser how many seconds it may cache the result. That avoids sending a new preflight for every subsequent complex request the client makes.