// Engineering Log
HTTPS and Web Server in Practice: Part 3 — What Is CORS: Browser Console Errors and Configuration in Nginx
Published on 2026-11-02 · Author: Mikhail Gorbachev
// Fast route
This article belongs to the topic Servers and infrastructure.
CORS (Cross-Origin Resource Sharing) — a mechanism by which a server allows JavaScript code from other sites to read its responses. By default the browser does not allow this: a script on https://app.example.ru can send a request to https://api.example.ru, but will not be able to read the response unless the API server explicitly allowed it. Hence errors like “blocked by CORS policy” in the console, which appear as soon as the frontend and API are on different addresses. Below — how the check works, what each typical error means, and how to configure the server. The HTTP headers themselves are briefly covered in the article “HTTP headers: what they are and what they do”.
What is an “origin”
An origin is the scheme, host, and port together. Two addresses belong to the same origin only if all three parts match:
| Page URL | Request URL | Same origin? |
|---|---|---|
https://example.ru | https://example.ru/api | yes |
https://example.ru | https://api.example.ru | no — different host |
https://example.ru | http://example.ru | no — different scheme |
http://localhost:3000 | http://localhost:8000 | no — different port |
A subdomain is already a different origin. Local development with the frontend on port 3000 and the API on 8000 is also cross-origin.
What the browser blocks exactly
The same-origin policy forbids a script from reading responses from other origins. It usually does not forbid sending requests. CORS is a way to relax this rule for specific origins.
Three important implications:
- CORS protects the user, not the server. curl, Postman, a Python script, or another server do not check CORS and will always receive the response. You can restrict API access only by authentication;
- a “simple” request from another site reaches the server and is executed — the browser simply does not give the response to the script. Protection against forging such requests is not CORS but CSRF tokens and cookie
SameSite(“Attacks at the HTTP level”); - a CORS error is a server response missing the required headers, not a network failure. The server may have returned 200 or 500: the real status code is visible on the Network tab.
Simple requests and preflight request
The browser sends a simple request directly and checks the response headers. A request is considered simple if all of the following are true:
- method is GET, HEAD, or POST;
- headers are only from a short allowed list (
Accept,Accept-Language,Content-Language,Content-Type, and some others), withoutAuthorizationand customX-…headers; Content-Typeis onlyapplication/x-www-form-urlencoded,multipart/form-data, ortext/plain.
Any other request — PUT, PATCH, DELETE, JSON with Content-Type: application/json, a request with Authorization — requires a preflight. The browser first sends an OPTIONS:
OPTIONS /v1/orders HTTP/1.1
Host: api.example.ru
Origin: https://app.example.ru
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: authorization, content-typeThe server must respond with a 2xx status and permissions:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.ru
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 7200
Vary: OriginOnly after this the real PUT is sent, and its response must also contain Access-Control-Allow-Origin. Access-Control-Max-Age allows the browser to cache the preflight result; Chrome limits this to two hours.
Response headers
Access-Control-Allow-Origin— which origin can read the response: a specific one (https://app.example.ru) or any (*). The value is singular: a comma-separated list is not allowed;Access-Control-Allow-Methods,Access-Control-Allow-Headers— allowed methods and headers, only in response toOPTIONS;Access-Control-Allow-Credentials: true— allow requests with cookies andAuthorization;Access-Control-Expose-Headers— which response headers are available to the script. By default the script sees only a few basic ones (Content-Type,Content-Length,Cache-Control, etc.); to readX-Total-CountorContent-Dispositionthey must be listed;Access-Control-Max-Age— caching the preflight request, in seconds;Vary: Origin— required ifAllow-Origindepends on the requestOrigin. Otherwise a CDN or browser may cache a response with permission for one site and give it to another.
Requests with cookies and authorization
By default fetch does not send cookies to another origin. To send them:
fetch("https://api.example.ru/v1/me", { credentials: "include" });Then the server must:
- return a specific origin in
Access-Control-Allow-Origin—*is forbidden together with credentials; - return
Access-Control-Allow-Credentials: true; - have a proper
SameSiteon the cookie: for a different site —SameSite=None; Secure. For subdomains of the same site (app.example.ruandapi.example.ru— one “site” in terms ofSameSite)Laxis sufficient.
A token in the Authorization header is simpler than a cookie: no credentials: "include" needed, but Authorization must be included in Access-Control-Allow-Headers.
Console errors and what they mean
No 'Access-Control-Allow-Origin' header is present on the requested resource
The server did not return the header. Reasons: CORS is not configured; it is configured but the response came with an error (404, 500, 502), and headers are added only to successful responses — in Nginx always is missing; the request went to the wrong address. Check the real response code on the Network tab.
Response to preflight request doesn't pass access control check: It does not have HTTP ok status
The response to OPTIONS is not 2xx. Most often OPTIONS requires authentication (401), is not supported by the framework (404, 405), or is redirected. The preflight always goes without cookies and Authorization — the server must answer it without access checks.
Redirect is not allowed for a preflight request
A redirect was returned for OPTIONS: from http to https, from a path with a slash to without (/v1/orders → /v1/orders/), or to the login page. Fix the frontend address so there is no redirect.
The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'
The request includes cookies, but the response has *. A specific origin and Allow-Credentials: true are required.
Request header field authorization is not allowed by Access-Control-Allow-Headers in preflight response
The header is not listed in Access-Control-Allow-Headers. Same for content-type when sending JSON and for any custom headers.
Method PUT is not allowed by Access-Control-Allow-Methods in preflight response
The method is not listed in Access-Control-Allow-Methods.
The 'Access-Control-Allow-Origin' header contains multiple values '…, …', but only one is allowed
The header is added by two layers at once — for example, both Nginx and the application, or both the application and a CDN. Keep CORS in one place.
The response arrives, but response.headers.get("X-Total-Count") returns null
The header is not listed in Access-Control-Expose-Headers.
Configuration in Nginx
Allowed origins — by list using map, so you don’t echo any Origin:
map $http_origin $cors_origin {
default "";
"https://app.example.ru" $http_origin;
"https://admin.example.ru" $http_origin;
"~^http://localhost:\d+$" $http_origin; # only for the test environment
}
server {
listen 443 ssl;
server_name api.example.ru;
location / {
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 "Authorization, Content-Type, X-Request-ID" always;
add_header Access-Control-Max-Age 7200 always;
add_header Vary Origin always;
return 204;
}
add_header Access-Control-Allow-Origin $cors_origin always;
add_header Access-Control-Allow-Credentials true always;
add_header Access-Control-Expose-Headers "X-Total-Count, Content-Disposition" always;
add_header Vary Origin always;
proxy_pass http://127.0.0.1:8080;
}
}Notes:
ifwithreturninsidelocationis one of the acceptable uses ofifin Nginx;- if the origin is not in the list,
$cors_originis empty, and Nginx does not emit a header with an empty value — the browser will block reading the response; alwaysis needed so headers are present in 4xx/5xx responses as well — otherwise the frontend will see a CORS error instead of a clear API error;- if another
add_headerappears in thelocation, headers defined at theserverlevel will stop working there — keep all headers in one place.
Configuration in the application
If CORS is configured in the application, Nginx should not add headers at all:
- Express: the
corspackage —app.use(cors({ origin: ["https://app.example.ru"], credentials: true })); - Django:
django-cors-headers—CORS_ALLOWED_ORIGINS = ["https://app.example.ru"],CORS_ALLOW_CREDENTIALS = True; - FastAPI:
CORSMiddlewarewithallow_origins,allow_credentials,allow_methods,allow_headers; - Spring:
@CrossOriginor a globalCorsConfigurationSource.
The CORS middleware in the application must run before authentication, otherwise OPTIONS will get a 401.
How to avoid CORS
The simplest solution is to avoid cross-domain requests:
- one domain for frontend and API:
https://example.ru/— the site,https://example.ru/api/— the API served through the same Nginx. The origin is the same, CORS is not needed, cookies work withoutSameSite=None; - proxy in development: in Vite —
server.proxy, in Create React App — theproxyfield inpackage.json. The frontend calls/apion its own port, and the dev server forwards requests to the API.
How to test
You can reproduce the preflight with curl:
curl -si -X OPTIONS https://api.example.ru/v1/orders \
-H 'Origin: https://app.example.ru' \
-H 'Access-Control-Request-Method: PUT' \
-H 'Access-Control-Request-Headers: authorization, content-type' \
| grep -i '^access-control\|^HTTP\|^vary'And the main request — with the same Origin:
curl -si https://api.example.ru/v1/orders -H 'Origin: https://app.example.ru' | grep -i '^access-control'There should be no Access-Control-Allow-Origin header for an origin not in the allowed list.
Common mistakes
Access-Control-Allow-Originis taken from the requestOriginwithout checking against a list — this effectively allows any site, including with cookies.Access-Control-Allow-Origin: *on an API that uses cookie-based authentication — does not work and indicates CORS was configured blindly.- No
Vary: Originwhen the response depends onOrigin— a CDN serves a response with someone else’s permission. - CORS configured both in Nginx and the application — duplicate header.
OPTIONSprotected by authentication or rate limiting — the entire frontend breaks under any load.- Attempting to “fix CORS” on the frontend (
mode: "no-cors") — the request is sent, but the response becomes completely unavailable to the script.
// Similar task
If you are dealing with something similar
This article belongs to one of the main working topics. You can keep reading on the topic, go to the homepage to understand what I do, or open the service pages directly.
Article topic
Servers and infrastructure
VPS, Linux, web stack, migrations, hosting, databases, and core operations.
Typical tasks behind this topic
- Move a site or service to a new server
- Set up Linux, Nginx, databases, and backups
- Figure out why the system behaves unstably
// Next step
If you need help with this topic, not just another article, it is better to go straight to the service page. The homepage and topic collection stay available as secondary routes.
Open services// Reviews
Related reviews
Thanks to Mikhail for his responsiveness. We spoke by phone; he explained how I could do it myself. This is my second time reaching out — everything’s great and prompt.
Thanks to Mikhail for his responsiveness. We had a call; he explained how I could do it myself. This is my second time contacting him; everything is great and prompt.
Consultation on Nginx Proxy Manager and Portainer
2025-02-25 · ★ 5/5
I want to express my deep gratitude to the specialist who set up SEO-friendly URLs for me on OpenCart. Configuring them turned out to be easy and simple, and I'm glad I finally found a professional who did everything well and without unnecessary complications. Before this I went through four specialists, and each time there were issues with the setup, but this person handled the task perfectly.
I want to express my great gratitude to the specialist who configured SEO-friendly URLs for me on OpenCart. Setting up the URLs turned out to be easy and simple, and I'm glad that I finally found a professional who did …
Excellent work — this isn't the first time I've used them; they find solutions to complex problems. I recommend.
Excellent work, this isn't my first time contacting them; they find solutions to complex problems. I recommend.
Excellent work! He completed the assigned task on time and without errors. It was a pleasure to work with him; I recommend him.
Great job! Completed the assigned task on time and without errors. It was a pleasure to work together, I recommend.
Excellent specialist — he delved into the problem, figured it out, and fixed it. I recommend him.
Excellent specialist, delved into the problem, figured it out, and fixed it. I recommend.
I needed to resolve an SSL certificate issue on a server where the certificate had been issued through Nginx Proxy Manager. Mikhail clarified all the details of my setup and requested access to assess the feasibility of solving the issue, since he hadn't worked with that service before. He quickly figured it out and fixed my problem. Perfect cooperation)
I needed to solve an issue with an SSL certificate on the server that was issued through Ngnix Proxy manager. Mikhail clarified all the details of how everything is set up for me, asked for access to assess the …
Diagnosing Nginx Proxy Manager in a Docker container and resolving the issue
2024-03-15 · ★ 5/5
// Contact
Need help?
Get in touch with me and I'll help solve the problem
I reply within one business day (03:00-13:00 GMT)
Or leave a request here:
// Related