What CORS is, in one paragraph
Browsers enforce the same-origin policy: a script on one origin can send requests to another origin, but it cannot read the response unless that other origin says it is allowed. An origin is the combination of scheme, host, and port, so https://app.example.com, https://api.example.com, and http://localhost:3000 are three different origins. Cross-Origin Resource Sharing (CORS) is the set of HTTP headers a server uses to grant that permission.
CORS is enforced by browsers, not by servers. That is why the same request can succeed in curl, a desktop API client, or server-side code, and still fail on a web page.
Reading the error message
CORS failures appear in the browser console, not in your JavaScript error object, which only reports a generic network failure. The console message tells you which check failed. Two of the most common, in Chrome’s wording:
Access to fetch at 'https://api.example.com/orders' from origin 'https://app.example.com'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on
the requested resource.
Access to fetch at 'https://api.example.com/orders' 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.The first means the actual response came back without permission. The second means the browser never sent your real request, because the preflight check that precedes it failed.
Simple requests and preflight requests
Some requests are considered simple: GET, HEAD, or POST using only a short list of safe headers, with a Content-Type of text/plain, multipart/form-data, or application/x-www-form-urlencoded. The browser sends these directly and then checks the response headers before handing the response to your code.
Everything else triggers a preflight: an automatic OPTIONS request that asks the server whether the real request is allowed. Sending JSON with Content-Type: application/json, adding an Authorization header, or using PUT, PATCH, or DELETE all cause a preflight.
OPTIONS /orders HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: authorization, content-type
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 600Common causes and fixes
| Symptom | Likely cause | Fix on the server |
|---|---|---|
| No Access-Control-Allow-Origin header | The API does not send CORS headers for your origin | Return Access-Control-Allow-Origin with your exact origin |
| Preflight fails with 401 or 403 | Authentication middleware rejects the OPTIONS request | Answer OPTIONS before authentication runs |
| Preflight fails, header not allowed | A custom header is missing from the allow list | Add it to Access-Control-Allow-Headers |
| Works without cookies, fails with them | Wildcard origin used with credentials | Return the specific origin and Access-Control-Allow-Credentials: true |
| Fails only from the HTTPS site | Mixed content: an http:// API called from an https:// page | Serve the API over HTTPS |
Credentials deserve special attention. When a request includes cookies or uses credentials: 'include', the server must name the exact origin; Access-Control-Allow-Origin: * is rejected in that case. It must also send Access-Control-Allow-Credentials: true.
Debugging CORS step by step
- Open your browser’s developer tools, select the Network tab, and reproduce the failing request.
- Look for an
OPTIONSrequest to the same URL just before your real request. If it is there, the browser sent a preflight; check its status code and response headers first. - Compare the
Originrequest header with theAccess-Control-Allow-Originresponse header. They must match exactly, including scheme and port, unless the server returns*for a request without credentials. - For preflights, confirm that every method and header your request uses appears in
Access-Control-Allow-MethodsandAccess-Control-Allow-Headers. - If cookies or credentials are involved, confirm
Access-Control-Allow-Credentials: trueand a specific origin. - Make the same request with curl. If it fails there too, the problem is not CORS at all: the server is returning an error, and the missing headers are a side effect.
That last step is worth remembering. Many servers only add CORS headers to successful responses, so a 500 error or a crash can show up in the browser as a CORS failure. Fix the underlying error, and configure CORS headers on error responses too, so the browser can show you the real status code.
Server configuration examples
In Node.js with Express, the cors package handles both preflight and actual responses. List allowed origins explicitly instead of reflecting whatever origin arrives.
import express from 'express';
import cors from 'cors';
const app = express();
app.use(cors({
origin: ['https://app.example.com', 'https://admin.example.com'],
credentials: true,
}));With nginx in front of your API, add the headers and answer preflight requests directly:
location /api/ {
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;
if ($request_method = OPTIONS) {
return 204;
}
proxy_pass http://backend;
}Fixing CORS during local development
The cleanest development setup avoids cross-origin requests entirely. Most front-end dev servers can proxy API calls, so the browser talks to one origin and the dev server forwards requests to the backend. In Vite, for example:
export default {
server: {
proxy: {
'/api': 'http://localhost:3001',
},
},
};Avoid browser flags or extensions that disable web security altogether. They hide real configuration problems until production and make everyday browsing less safe.
Localhost, VPNs, and private networks
Requests from a public website to localhost, a company VPN host, or a private IP address face extra hurdles. Many internal services do not send CORS headers at all, an https:// page cannot call plain http:// addresses other than localhost because of mixed-content rules, and browsers are adding further protections for requests from public sites to local networks, which can appear as permission prompts or additional preflight requirements.
When you control the internal service, add proper CORS headers and serve it over HTTPS. When you are only testing an API you cannot change, use a tool that sends requests outside the page context. The UtiliStudio REST Client sends requests directly from your browser when the target allows it, and its optional browser extension relays requests from your own machine for localhost, VPN, and CORS-restricted endpoints.
Security mistakes to avoid
- Reflecting any origin. Copying the incoming
Originheader intoAccess-Control-Allow-Originwithout checking it against an allow list effectively disables the protection. - Wildcards with credentials. Browsers reject
*with credentials for good reason; do not work around it by reflecting origins. - Treating CORS as authentication. CORS only controls which web pages can read responses. Non-browser clients ignore it, so every endpoint still needs real authentication and authorization.
- **Allowing
nullas an origin.** Sandboxed iframes and local files sendOrigin: null; allowing it opens the door to any such page.
Keep learning
- HTTP status codes explained: a practical reference
- JWT explained: structure, claims, and security pitfalls
- Developer Utilities: Essential developer helpers including hash generators, base64 encoders, and regex testers.