Skip to content
Commitpedia
Log in Join & edit

CORS explained, and how to fix the browser error

The browser blocks a request from one origin to another unless the server allows it with Access-Control headers. Fix it in the app or proxy with the right headers and preflight response, or remove the cross-origin hop.

Drafted with AI from official documentation. Scripts were run in a test environment and repository links verified. Awaiting a human check. Did it work for you? Last edited Outdated?

Edit history of CORS explained, and how to fix the browser error: 1 recorded edit
Edit history · 1 edit
Type
Article
Edits
1
Editors
1
First issued
15 Oct 2026
Last edit
15 Oct 2026
Reading
5 min
Access to fetch at 'https://api.example.com/users' from origin 'https://app.example.com'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present
on the requested resource.

Cross-Origin Resource Sharing (CORS) is a browser rule. A page served from one origin (scheme + host + port, so https://app.example.com and http://localhost:5173 are different origins) may only read responses from another origin if that server explicitly allows it. The server does so with response headers. The request itself usually reaches the server and gets processed; it is the browser that refuses to hand the response to your JavaScript. That is why curl works and the page does not, and why the fix is always on the server side, never in the frontend code.

What the browser does

  • For a simple request (GET, HEAD or POST with a form-style content type and no custom headers) the browser sends it and then checks the response for Access-Control-Allow-Origin. If it is missing or does not match, the response is discarded.
  • For anything else (PUT, DELETE, JSON bodies, an Authorization header) the browser first sends a preflight: an OPTIONS request with Access-Control-Request-Method and Access-Control-Request-Headers. The server must answer with the matching Allow- headers and a 2xx status, otherwise the real request is never sent.
  • If the request includes cookies or HTTP auth (credentials: 'include'), the server must also send Access-Control-Allow-Credentials: true and may not use * as the allowed origin.

Read the error message

  • No ‘Access-Control-Allow-Origin’ header is present: the server did not send the header at all. Often the API framework has a CORS setting that is off, or the proxy in front strips it, or the response was an error page from the proxy (a 502 or 404) that never had the header.
  • The ‘Access-Control-Allow-Origin’ header has a value ‘https://other’ that is not equal to the supplied origin: the allow list exists but does not contain this origin. Check the exact scheme, host and port, including www.
  • Response to preflight request doesn’t pass access control check: the OPTIONS request failed. Typically the route requires authentication and returns 401 to the preflight, or no route handles OPTIONS at all.
  • Request header field authorization is not allowed by Access-Control-Allow-Headers: add that header name to Access-Control-Allow-Headers.
  • The value of the ‘Access-Control-Allow-Origin’ header must not be the wildcard ‘*’ when the request’s credentials mode is ‘include’: echo the specific origin instead of *.

Confirm what the server really sends, from a terminal, with the same Origin the browser would use:

curl -sI -H 'Origin: https://app.example.com' https://api.example.com/users | grep -i access-control

curl -sI -X OPTIONS https://api.example.com/users \
  -H 'Origin: https://app.example.com' \
  -H 'Access-Control-Request-Method: PUT' \
  -H 'Access-Control-Request-Headers: authorization, content-type'

Fix 1: enable CORS in the application

Every web framework has a CORS module. Use it rather than setting headers by hand, because it also handles the preflight and the Vary: Origin header. Allow only the origins you own:

// Express
import cors from 'cors';
app.use(cors({
  origin: ['https://app.example.com', 'http://localhost:5173'],
  credentials: true,
}));
# FastAPI
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://app.example.com", "http://localhost:5173"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

Django uses the django-cors-headers package with CORS_ALLOWED_ORIGINS; Spring has @CrossOrigin and a global CorsConfiguration; Rails uses rack-cors. Make sure the middleware runs before authentication so the preflight is not rejected with 401.

Fix 2: set the headers in the reverse proxy

When you cannot change the application, or want one place for the rule, the proxy can answer the preflight and add the headers. In Nginx:

location /api/ {
    if ($request_method = OPTIONS) {
        add_header Access-Control-Allow-Origin  "https://app.example.com" always;
        add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
        add_header Access-Control-Allow-Headers "Authorization, Content-Type" always;
        add_header Access-Control-Max-Age 86400 always;
        return 204;
    }
    add_header Access-Control-Allow-Origin "https://app.example.com" always;
    add_header Vary Origin always;
    proxy_pass http://127.0.0.1:8000;
}

always makes Nginx add the header to error responses too, so a 500 from the backend still carries it and the browser shows the real error instead of a CORS error. If the backend already sends its own Access-Control-Allow-Origin, remove one of the two: a response with two values is rejected. Use proxy_hide_header Access-Control-Allow-Origin; to drop the backend’s copy.

In Caddy:

api.example.com {
    @options method OPTIONS
    handle @options {
        header Access-Control-Allow-Origin  "https://app.example.com"
        header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
        header Access-Control-Allow-Headers "Authorization, Content-Type"
        header Access-Control-Max-Age 86400
        respond 204
    }
    header Access-Control-Allow-Origin "https://app.example.com"
    header Vary Origin
    reverse_proxy localhost:8000
}

Fix 3: make it not cross-origin

The cleanest solution is often to avoid the cross-origin hop entirely. Serve the frontend and the API from the same origin and route by path at the proxy, so the page at https://app.example.com calls /api/users on the same host:

app.example.com {
    handle_path /api/* {
        reverse_proxy localhost:8000
    }
    handle {
        root * /var/www/app
        try_files {path} /index.html
        file_server
    }
}

No CORS headers are needed, cookies work without SameSite=None, and there are no preflights, which also makes the app faster. In development, Vite, webpack dev server and Create React App have a proxy setting that does the same thing for localhost.

Mistakes to avoid

  • Access-Control-Allow-Origin: * on an API that uses cookies or tokens. The browser will refuse credentialed requests, and if it did not, every website could call your API as the logged-in user.
  • Reflecting whatever Origin the request sends, with credentials enabled. That is equivalent to allowing everyone. Check against a list.
  • Forgetting Vary: Origin when the allowed origin depends on the request; caches and CDNs may otherwise serve one origin’s header to another.
  • Installing a browser extension that disables CORS. It hides the problem on your machine and does nothing for your users.
  • Treating a CORS error as the root cause when the backend actually returned 502 or 404. Look at the Network tab: the status code and whether the response has the header tell you which it is.

Official documentation

Did this work for you?

No votes yet. Be the first.

If it did not work, the report form below tells the editors what went wrong.

See also

  • 0019 fetch with timeout, retries and proper error handling A reusable fetchJson helper for Node 20+ and browsers that adds a timeout, retries with backoff for temporary failures, and clear errors… Intermediate · Step by step · Any system Script Async code · Making requests 15 Oct 2026
  • 0081 How an HTTP request works: methods, headers, and body An HTTP request is a short text message a client sends to a server: a method and path, some headers, and sometimes… Beginner · Explains an idea · Any system Article Making requests 15 Oct 2026
  • 0083 Test a JSON API with curl A runnable bash script showing the curl commands you need most when testing an API: GET, POST JSON, custom headers, Bearer tokens,… Beginner · Step by step · Linux, macOS, Windows Script Authentication · Making requests 15 Oct 2026
  • 0084 Tiny JSON echo server for testing HTTP clients A standard-library Python server that echoes back the method, path, query, headers, and body of every request, and can return any status… Intermediate · Step by step · Any system Script Files & automation · Making requests 15 Oct 2026
  • 0085 fetch with timeout, retries, and exponential backoff A drop-in fetch wrapper that times out slow requests, retries network errors and 429/5xx responses with jittered exponential backoff, and honors Retry-After. Advanced · Step by step · Any system, Browser Script Async code · Making requests 15 Oct 2026
  • 0087 CORS explained: why the browser blocks your request CORS is the browser rule that decides whether JavaScript on one origin may read a response from another. The server opts in… Intermediate · Explains an idea · Browser Article Async code · Making requests 15 Oct 2026

Repos in the wild

  • Requests: HTTP for humans psf/requests Requests is the classic Python library for calling web APIs and downloading pages, with a simple API for GET and POST requests, headers, JSON and sessions. GitHub
  • Axios axios/axios A promise-based HTTP client for browsers and Node.js with interceptors, automatic JSON handling, request cancellation and a consistent API in both environments. GitHub
  • curl and libcurl (curl/curl) curl/curl The source of curl, the command-line tool for transferring data with URLs, and libcurl, the C library behind it that many other programs use for HTTP. GitHub
  • Caddy: the web server with automatic HTTPS caddyserver/caddy Caddy is an open-source web server and reverse proxy written in Go that obtains and renews TLS certificates on its own, with a configuration file short enough to memorize and HTTP/1, 2 and 3 support out of the box. GitHub

History

Every saved edit is kept. The plot on the sleeve draws one line per edit. Open any edit to see exactly what changed.

  1. Dominik +7,359 chars

Report a problem

Something wrong, unsafe or copied? Tell the editors.
What is wrong?