---
title: CORS
description: Cross-Origin Resource Sharing is a browser mechanism that lets a server opt in to relaxing the Same-Origin Policy for a chosen origin using HTTP headers on simple requests and OPTIONS preflights.
tokens: ~1410
---

# CORS

Cross-Origin Resource Sharing (CORS) is a browser mechanism that lets a server opt in to relaxing the [Same-Origin Policy](/docs/web/http/same-origin-policy) for a specific origin. The browser adds an `Origin` header to cross-origin requests, and JavaScript can read the response only when the server replies with matching `Access-Control-*` headers. 

Only the **browser** enforces this and it's not CSRF protection.

## Same-Origin Policy

By default a page at `https://app.example.com` cannot read a response from `https://api.example.com`. CORS does not replace that rule; the server opts in by naming the allowed origin (or `*`) in `Access-Control-Allow-Origin`.

Same-Origin Policy still allows some cross-origin *writes* without CORS — for example a form POST. That is why [CSRF](/docs/web/security/csrf) exists. CORS is the mechanism that, if misconfigured (`Access-Control-Allow-Origin` reflecting any `Origin` plus credentials), can also expose response bodies to another origin.

## Simple vs preflight

A **simple request** (CORS-safelisted) is sent immediately. The browser still hides the response from JavaScript unless the response includes a matching `Access-Control-Allow-Origin`. The request itself already reached the server.

A request is simple when all of these hold:

- Method is `GET`, `HEAD`, or `POST`
- Headers are only CORS-safelisted request headers (`Accept`, `Accept-Language`, `Content-Language`, and `Content-Type`)
- `Content-Type`, if present, is `application/x-www-form-urlencoded`, `multipart/form-data`, or `text/plain`

Anything else is **preflighted**. The browser first sends `OPTIONS` (shown as "Preflight" in DevTools). The actual request goes out only if that OPTIONS response allows it. `Authorization`, `application/json`, `PUT`, `PATCH`, and `DELETE` all trigger a preflight.

## Preflight request headers

The browser sets these on the OPTIONS request to describe the actual request:

| Header | Role |
| --- | --- |
| `Origin` | The page's origin |
| `Access-Control-Request-Method` | Method of the actual request |
| `Access-Control-Request-Headers` | Non-safelisted headers the actual request will send |

## Response headers

The server sets these on the preflight response, and repeats `Access-Control-Allow-Origin` (and credentials, if used) on the actual response:

| Header | Role |
| --- | --- |
| `Access-Control-Allow-Origin` | A single origin, or `*`. Not a list. |
| `Access-Control-Allow-Methods` | Methods allowed for the actual request |
| `Access-Control-Allow-Headers` | Headers allowed on the actual request (answers `Access-Control-Request-Headers`) |
| `Access-Control-Allow-Credentials` | `true` if cookies or HTTP auth may be included |
| `Access-Control-Max-Age` | Seconds the browser may cache this preflight |
| `Access-Control-Expose-Headers` | Response headers JavaScript may read, beyond the safelisted set (`Cache-Control`, `Content-Language`, `Content-Length`, `Content-Type`, `Expires`, `Last-Modified`, `Pragma`) |

`Allow-Headers` and `Expose-Headers` are different allowlists: one for request headers the client wants to *send*, one for response headers the client wants to *read*.

To allow more than one origin, echo a known `Origin` back as `Access-Control-Allow-Origin` instead of listing several values.

When that value depends on the request `Origin`, send `Vary: Origin` so caches do not reuse one origin's response for another.

## Credentials and `*`

Credentialed requests (`fetch(..., { credentials: "include" })`, or XHR `withCredentials`) send [cookies](/docs/web/http/cookies) and HTTP auth. The response must use a specific origin, not `*`:

```text
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
```

`Access-Control-Allow-Origin: *` together with `Access-Control-Allow-Credentials: true` is rejected by the browser. A `Set-Cookie` on a response whose `Access-Control-Allow-Origin` is `*` also does not set a cookie.

## `no-cors` and proxies

`fetch(url, { mode: "no-cors" })` still sends the request, but the result is an **opaque** response: JavaScript cannot read the status, body, or headers. It does not bypass SOP.


## Simple request example

Page at `https://app.example.com` POSTs a form to `https://api.example.com/data`. `Content-Type` is `application/x-www-form-urlencoded`.

```http
POST /data HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Content-Type: application/x-www-form-urlencoded

name=Ada
```

```http
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com
Content-Type: application/json

{"ok": true}
```

Without `Access-Control-Allow-Origin`, this POST still hits the server; JavaScript just cannot read the JSON.

## Preflighted request

Page at `https://app.example.com` POSTs JSON to `https://api.example.com/users` with a cookie.

```http
OPTIONS /users HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type
```

```http
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: content-type
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 86400
Vary: Origin
```

Then the actual request:

```http
POST /users HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Content-Type: application/json
Cookie: session=abc

{"name": "Ada"}
```

```http
HTTP/1.1 201 Created
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Vary: Origin
Content-Type: application/json

{"id": "1"}
```