# No 'Access-Control-Allow-Origin' Header Is Present: Fix CORS

> Fix the CORS error 'No Access-Control-Allow-Origin header is present on the requested resource' with working Express, nginx, S3, Workers and Django fixes.

Source: https://howhttpworks.com/debug/cors-no-access-control-allow-origin
Last reviewed: 2026-10-04

Error messages this page covers:
- `Access to fetch at 'https://api.example.com/items' from origin 'https://app.example.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.`
- `Access to XMLHttpRequest at 'https://api.example.com/items' from origin 'https://app.example.com' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.`
- `Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at https://api.example.com/items. (Reason: CORS header ‘Access-Control-Allow-Origin’ missing). Status code: 200.`
- `Origin https://app.example.com is not allowed by Access-Control-Allow-Origin. Status code: 200`

> **TL;DR:** The response to your cross-origin request had no `Access-Control-Allow-Origin` header matching the page's origin, so the browser hid it from your script. Add the header on the server that owns the resource, on every response including errors and the OPTIONS preflight. The client cannot fix this.

## What it means

When JavaScript on `https://app.example.com` calls `https://api.example.com`, the browser sends the request with an `Origin` header and then inspects the response. If the response does not contain `Access-Control-Allow-Origin` with either that exact origin or `*` (for requests without credentials), the browser discards the body and raises the error above. The request usually did reach the server. Side effects of a POST may have already happened.

This is the check defined in the [WHATWG Fetch Standard](https://fetch.spec.whatwg.org/#http-cors-protocol). The wording differs by browser, but the cause is the same:

```text
Chrome:  No 'Access-Control-Allow-Origin' header is present on the requested resource.
Firefox: (Reason: CORS header 'Access-Control-Allow-Origin' missing). Status code: 200.
Safari:  Origin https://app.example.com is not allowed by Access-Control-Allow-Origin. Status code: 200
         Fetch API cannot load https://api.example.com/items due to access control checks.
```

Firefox also prints `(Reason: CORS request did not succeed)` when there was no HTTP response at all, such as a DNS failure, a TLS error, a blocked mixed-content request or a connection reset. That variant is not a missing header; fix the network failure first.

## Who sent it?

The browser generated the message, but the missing header is the fault of whichever layer produced the response you received. Look at that response in DevTools, Network tab, rather than at the console message.

- Response has `Server: nginx` or your framework's header and a normal body: the origin application or its nginx is not adding the header.
- Response has `Server: cloudflare` and a `CF-Ray` header, or `X-Amz-Cf-Id` (CloudFront), `X-Amzn-Trace-Id` (API Gateway or ALB): a CDN or gateway answered. A 4xx or 5xx generated by the gateway itself will not carry your application's CORS headers.
- No response at all (status `(failed)` or `(blocked)`): it is not a header problem. Check for mixed content, an ad blocker, a certificate error, or an unreachable host.
- Status is a `301` or `302`: a redirect response has no CORS headers, and a preflight is not allowed to be redirected. Call the final URL directly (`https`, correct host, trailing slash).

Reproduce it outside the browser by sending the `Origin` header yourself, because many servers only emit CORS headers when `Origin` is present:

```bash
curl -si https://api.example.com/items \
  -H 'Origin: https://app.example.com' | grep -iE '^(HTTP|access-control|vary|server)'
```

A healthy answer looks like this:

```http
HTTP/2 200
access-control-allow-origin: https://app.example.com
vary: Origin
```

For a preflight, add the method and headers the browser would ask about:

```bash
curl -si -X OPTIONS https://api.example.com/items \
  -H 'Origin: https://app.example.com' \
  -H 'Access-Control-Request-Method: POST' \
  -H 'Access-Control-Request-Headers: content-type,authorization'
```

You want a 2xx (204 is typical) with `Access-Control-Allow-Origin`, `Access-Control-Allow-Methods` and `Access-Control-Allow-Headers` that cover what was requested.

## Fix it, in order of likelihood

1. Confirm which URL is failing. Compare the URL in the console message with the one you think you are calling. A typo in the host, `http` instead of `https`, or a missing `/api` prefix often lands on a different server or a 404 handler with no CORS configuration.
2. Add the header on the server that owns the resource, not on the client.
3. Make sure it is added to every response: errors (401, 404, 500), redirects, and the OPTIONS preflight. Middleware order matters; the CORS handler must run before auth and body parsing.
4. Reflect the request origin from an allowlist rather than hard-coding `*` if cookies or `Authorization` are involved, and send `Vary: Origin`.
5. If something sits in front (CDN, API gateway, load balancer), check that it forwards the `Origin` header and does not cache one origin's response for another.
6. Re-test with curl using the `Origin` header, then hard-reload the page. Browsers cache preflight results for `Access-Control-Max-Age` seconds (Chromium caps it at 7200 seconds, Firefox at 86400), so a fixed server can still look broken for a while.

### Express (cors package)

```javascript
import express from 'express'
import cors from 'cors'

const app = express()

const allowed = new Set(['https://app.example.com', 'https://admin.example.com'])

app.use(
  cors({
    origin(origin, callback) {
      // Requests with no Origin (curl, server-to-server) are not CORS requests
      if (!origin || allowed.has(origin)) return callback(null, true)
      callback(null, false) // omit the header; do not throw, or you get a 500 with no CORS headers
    },
    credentials: true, // sends Access-Control-Allow-Credentials: true
    methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
    allowedHeaders: ['Content-Type', 'Authorization'],
    maxAge: 600
  })
)

// Register routes AFTER cors(), otherwise early errors skip it
app.use('/items', itemsRouter)
```

`app.use(cors(...))` also answers OPTIONS preflights for every route. The cors package sets `Vary: Origin` itself when the origin is dynamic.

### nginx

The classic mistake is `add_header` without `always`. Without it, nginx only adds the header to 200, 201, 204, 206, 301, 302, 303, 304, 307 and 308 responses, so your 401, 404 and 502 pages look like CORS failures. A second trap: `add_header` directives are inherited from the enclosing level only if the current level defines none, so a single `add_header` inside a `location` (or `if`) silently discards the ones set above it. That is why the `if` block below repeats every header.

```nginx
map $http_origin $cors_origin {
    default                      "";
    "https://app.example.com"    $http_origin;
    "https://admin.example.com"  $http_origin;
}

server {
    listen 443 ssl;
    server_name api.example.com;

    location / {
        # An empty value means nginx does not emit the header at all
        add_header Access-Control-Allow-Origin      $cors_origin always;
        add_header Access-Control-Allow-Credentials "true" always;
        add_header Vary                             "Origin" always;

        if ($request_method = OPTIONS) {
            add_header Access-Control-Allow-Origin  $cors_origin always;
            add_header Access-Control-Allow-Credentials "true" always;
            add_header Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE, OPTIONS" always;
            add_header Access-Control-Allow-Headers "Content-Type, Authorization" always;
            add_header Access-Control-Max-Age       600 always;
            add_header Vary                         "Origin" always;
            return 204;
        }

        proxy_pass http://app_backend;
    }
}
```

If the upstream application already sends CORS headers, do not add them again in nginx. Duplicated `Access-Control-Allow-Origin` values are rejected by Chrome with a different error (`The 'Access-Control-Allow-Origin' header contains multiple values '...', but only one is allowed`). Use `proxy_hide_header Access-Control-Allow-Origin;` before `add_header` if you want nginx to own the policy.

### Cloudflare Workers

```javascript
const ALLOWED = new Set(['https://app.example.com'])

function corsHeaders(origin) {
  const h = new Headers()
  if (origin && ALLOWED.has(origin)) {
    h.set('Access-Control-Allow-Origin', origin)
    h.set('Access-Control-Allow-Credentials', 'true')
  }
  return h
}

export default {
  async fetch(request) {
    const origin = request.headers.get('Origin')

    if (request.method === 'OPTIONS') {
      const h = corsHeaders(origin)
      h.set('Vary', 'Origin, Access-Control-Request-Headers')
      h.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
      h.set('Access-Control-Allow-Headers', request.headers.get('Access-Control-Request-Headers') || 'Content-Type')
      h.set('Access-Control-Max-Age', '600')
      return new Response(null, { status: 204, headers: h })
    }

    const upstream = await fetch(request)
    // Response from fetch() is immutable; copy it to change headers
    const response = new Response(upstream.body, upstream)
    for (const [k, v] of corsHeaders(origin)) response.headers.set(k, v)
    response.headers.append('Vary', 'Origin') // append: the origin may already send Vary: Accept-Encoding
    return response
  }
}
```

### Amazon S3 (and CloudFront in front of it)

S3 only emits CORS headers when the request carries an `Origin` header and a rule matches. In the bucket's Permissions tab, under Cross-origin resource sharing (CORS), paste a JSON array of rules:

```json
[
  {
    "AllowedOrigins": ["https://app.example.com"],
    "AllowedMethods": ["GET", "HEAD", "PUT"],
    "AllowedHeaders": ["*"],
    "ExposeHeaders": ["ETag"],
    "MaxAgeSeconds": 3000
  }
]
```

With CloudFront in front, the distribution must forward `Origin` to S3 and include it in the cache key, otherwise CloudFront can cache a response made without `Origin` (no CORS headers) and serve it to everyone. Put `Origin` in the cache key with a cache policy that whitelists it, and forward it to S3 with the managed `CORS-S3Origin` origin request policy. Alternatively, add CORS headers at the edge with a response headers policy and skip S3 CORS.

### API Gateway

For an HTTP API, set the CORS configuration on the API (AllowOrigins, AllowMethods, AllowHeaders, optionally AllowCredentials, MaxAge). When it is configured, API Gateway answers preflights itself and applies its own headers to responses. For a REST API with Lambda proxy integration, "Enable CORS" only creates the OPTIONS mock; your Lambda must return `Access-Control-Allow-Origin` in its own response, and gateway-generated errors (403 from an authorizer, 429 throttling, 5xx) need CORS headers added through Gateway Responses.

### Next.js route handlers

```typescript
// app/api/items/route.ts
const ALLOWED = new Set(['https://app.example.com'])

function cors(origin: string | null) {
  const headers: Record<string, string> = { Vary: 'Origin' }
  if (origin && ALLOWED.has(origin)) {
    headers['Access-Control-Allow-Origin'] = origin
    headers['Access-Control-Allow-Credentials'] = 'true'
  }
  return headers
}

export async function OPTIONS(request: Request) {
  return new Response(null, {
    status: 204,
    headers: {
      ...cors(request.headers.get('origin')),
      'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
      'Access-Control-Allow-Headers': 'Content-Type, Authorization',
      'Access-Control-Max-Age': '600'
    }
  })
}

export async function GET(request: Request) {
  return Response.json({ items: [] }, { headers: cors(request.headers.get('origin')) })
}
```

For a single fixed origin you can instead set `headers()` in `next.config` for `/api/:path*`. It cannot reflect the origin from an allowlist.

### Django and Flask

```python
# Django: pip install django-cors-headers
INSTALLED_APPS = [..., 'corsheaders']
MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware',   # as high as possible, before CommonMiddleware
    'django.middleware.common.CommonMiddleware',
    ...
]
CORS_ALLOWED_ORIGINS = ['https://app.example.com']
CORS_ALLOW_CREDENTIALS = True
```

```python
# Flask: pip install flask-cors
from flask import Flask
from flask_cors import CORS

app = Flask(__name__)
CORS(app, origins=['https://app.example.com'], supports_credentials=True, max_age=600)
```

## Credentials and the wildcard rule

If the request uses `credentials: 'include'` (cookies, client certificates) the response must contain a specific origin, not `*`, plus `Access-Control-Allow-Credentials: true`. An `Authorization` header set manually in JavaScript is not "credentials" in the Fetch sense, but it does force a preflight, and the preflight must list `Authorization` by name in `Access-Control-Allow-Headers`: even without credentials, a `*` there does not cover it ([MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Access-Control-Allow-Headers)). On credentialed requests, `*` in `Access-Control-Allow-Headers` and `Access-Control-Allow-Methods` is treated as a literal string, not a wildcard. Name each one.

Never reflect an arbitrary `Origin` value back with credentials enabled. That makes every site on the internet a trusted origin for your logged-in users. Match against an allowlist.

## Why it works in Postman and curl

Nothing in the HTTP exchange changes. Postman and curl send the same request and receive the same bytes. They just do not apply the same-origin policy, which exists to protect users' browser sessions, not your API. A server that "works in Postman" has not proven it is CORS-correct. It has proven it is reachable. The only useful test is to replay the request with the same `Origin` header and, for non-simple requests, the same preflight.

## Related

- [Debugging CORS errors](https://howhttpworks.com/debug/cors-error) covers the other CORS failure messages.
- [Failed CORS preflight](https://howhttpworks.com/debug/cors-preflight) for `Response to preflight request doesn't pass access control check`.
- [Access-Control-Allow-Origin](https://howhttpworks.com/headers/access-control-allow-origin), [Access-Control-Allow-Credentials](https://howhttpworks.com/headers/access-control-allow-credentials) and [Vary](https://howhttpworks.com/headers/vary) for the header semantics.
- [CORS guide](https://howhttpworks.com/guides/cors) for the model behind simple versus preflighted requests.
- The [CORS debugger](https://howhttpworks.com/tools/cors-debugger) lets you test an endpoint with a chosen origin.
