CORS errors that only happen in production: read the message, then fix the cause.
In development the frontend and API usually share an origin, so CORS never runs. In production they don’t. The browser’s message names the problem: a preflight that didn’t get a 2xx, an origin that doesn’t match exactly, credentials with a * wildcard, a redirect, or an error response without CORS headers. Fix it on the API, not in the browser.
Facts checked
Why it works locally
CORS applies when a page on one origin (scheme, host and port) calls another. Locally, your dev server often proxies /api to the backend, so the browser only ever talks to localhost:5173. Or the API allows http://localhost:3000 and nothing else. In production the frontend is on https://example.ca and the API on https://api.example.ca or https://your-api.canner.app: two origins, and the browser now asks the API for permission.
Treat “CORS error” as a symptom. The browser hides the response from your code, so a crashed endpoint, a 404 and a real policy mismatch all look the same in the console until you open the request.
What the message says, and what fixes it
Chrome’s messages start with Access to fetch at '…' from origin '…' has been blocked by CORS policy:. The rest of the line is the part that matters:
| The message continues | Cause | Fix |
|---|---|---|
No 'Access-Control-Allow-Origin' header is present on the requested resource. † † | The response had no CORS headers: the origin isn’t on the API’s list, or the response came from something that runs before your CORS middleware (a crash, a 404 for the route, a proxy error page) | Open the request in the Network tab and read its real status first. Add the origin; register CORS middleware before auth and routes |
Response to preflight request doesn't pass access control check: It does not have HTTP ok status. † † | The OPTIONS preflight got a non-2xx answer. Preflights never carry cookies or tokens, so auth middleware often answers 401 | Let OPTIONS through before authentication; return 204 with the CORS headers |
The 'Access-Control-Allow-Origin' header has a value 'https://example.ca' that is not equal to the supplied origin. † † | The origin isn’t an exact match: www vs apex, http vs https, a port, or a trailing slash in your config | Compare the request’s Origin header with your list, character for character. Origins never end in / |
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'. † † | credentials: 'include' (or withCredentials) with Access-Control-Allow-Origin: * | Echo the specific origin, send Access-Control-Allow-Credentials: true and Vary: Origin. Or drop credentials if you use bearer tokens |
Redirect is not allowed for a preflight request. † † | The API URL redirects: http to https, www to apex, or a trailing-slash redirect | Call the final URL directly. curl -I the exact URL your frontend uses |
Request header field authorization is not allowed by Access-Control-Allow-Headers in preflight response. † † | The request sends a header the preflight answer didn’t list | Add it to Access-Control-Allow-Headers. With credentials, * doesn’t count: list names |
† Competitor details come from each vendor’s own published pages on the dates shown; prices and plans change, so check the vendor before you decide. `No 'Access-Control-Allow-Origin' header is present on the requested resource.` (2026-10-29) · `No 'Access-Control-Allow-Origin' header is present on the requested resource.` (2026-10-29) · `Response to preflight request doesn't pass access control check: It does not have HTTP ok status.` (2026-10-29) · `Response to preflight request doesn't pass access control check: It does not have HTTP ok status.` (2026-10-29) · `The 'Access-Control-Allow-Origin' header has a value 'https://example.ca' that is not equal to the supplied origin.` (2026-10-29) · `The 'Access-Control-Allow-Origin' header has a value 'https://example.ca' that is not equal to the supplied origin.` (2026-10-29) · `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'.` (2026-10-29) · `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'.` (2026-10-29) · `Redirect is not allowed for a preflight request.` (2026-10-29) · `Redirect is not allowed for a preflight request.` (2026-10-29) · `Request header field authorization is not allowed by Access-Control-Allow-Headers in preflight response.` (2026-10-29) · `Request header field authorization is not allowed by Access-Control-Allow-Headers in preflight response.` (2026-10-29)
When the browser sends a preflight
A GET, HEAD or POST with only safelisted headers and a Content-Type of application/x-www-form-urlencoded, multipart/form-data or text/plain goes straight through. Anything else is preflighted first: PUT, PATCH, DELETE, a JSON body, an Authorization header, any custom X- header. That is why a form post works and the same call from fetch() with JSON fails.
The preflight is an OPTIONS request with Access-Control-Request-Method and Access-Control-Request-Headers. Access-Control-Max-Age lets the browser cache the answer; without it the default is 5 seconds, so a slow API pays for two round trips on most calls.
Origins on Canner: www, apex and preview URLs
- `www` and the apex are different origins. With Serve www on, Canner answers
www.example.cawith a 301 toexample.ca(custom domains). A frontend that callshttps://www.example.ca/apigets that redirect, and a preflight can’t follow it. Point API calls at the host that serves the app. - Plain `http://` is redirected to HTTPS at the proxy, which also breaks a preflight. An
http://API URL on an HTTPS page is blocked as mixed content anyway (proxy headers and mixed content). - Previews have their own origin. A pull-request preview runs at
branch--project.canner.app. If the frontend is a separate project, its previews call your API fromhttps://<branch>--web.canner.app, which isn’t on a list written for production. - Previews share the project’s environment variables. An
ALLOWED_ORIGINSset for production applies to previews too. Canner setsCANNER_ENV=previewandCANNER_URLto the preview’s address (environment variables), so code can tell them apart. For a Vite frontend, remember the API URL is baked in at build time (Vite env in production). - **Never allow every
*.canner.apporigin.** Other customers’ apps live there. Match your own project’s preview pattern, as in the code below.
// Express, with the cors package. ALLOWED_ORIGINS="https://example.ca,https://admin.example.ca"
const allowed = (process.env.ALLOWED_ORIGINS ?? "").split(",").filter(Boolean);
// Pull-request previews of the frontend project "web": https://<branch>--web.canner.app
const preview = /^https:\/\/[a-z0-9-]+--web\.canner\.app$/;
app.use(cors({
origin: (origin, cb) => cb(null, !origin || allowed.includes(origin) || preview.test(origin)),
credentials: true, // only if the browser sends cookies
}));
# FastAPI
app.add_middleware(
CORSMiddleware,
allow_origins=os.environ.get("ALLOWED_ORIGINS", "").split(","),
allow_origin_regex=r"https://[a-z0-9-]+--web\.canner\.app",
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)Proxies, caches and errors without headers
Canner’s proxy doesn’t add, remove or change Access-Control-* headers, and it passes OPTIONS requests through to your running app. Whatever CORS headers reach the browser are the ones your app sent. That cuts both ways: if your app crashes and answers 500 from an error handler that runs outside the CORS middleware, or doesn’t answer and the proxy returns a 502, the response has no CORS headers and the browser reports a CORS error. Check the app’s logs for that request before touching the policy.
Caches are the other trap. When the API reflects the request’s origin, it must send Vary: Origin, or a shared cache can hand one site’s Access-Control-Allow-Origin to another. Canner’s optional cache never stores a response that varies on Origin, so it can’t cause this; a CDN you put in front yourself might.
Debug it in the browser, then with curl
- Network tab, not the console. Find the failed request. If there’s an
OPTIONSrow before it, that’s the preflight: check its status and response headers. - Compare the `Origin` request header with
Access-Control-Allow-Originin the response. They must match exactly. - Right-click → Copy as cURL to replay the request outside the browser, then send the preflight yourself (below). curl ignores CORS, so it shows what the server really returns.
- Set the allowed origins as a variable and change them without a code change:
canner env set ALLOWED_ORIGINS=https://example.ca,https://admin.example.cawith the CLI, then redeploy.
# Send the preflight the browser would send
curl -i -X OPTIONS https://api.example.ca/items \
-H "Origin: https://example.ca" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: content-type, authorization"
# A good answer: a 2xx status and, at least,
# Access-Control-Allow-Origin: https://example.ca
# Access-Control-Allow-Methods: ... POST ...
# Access-Control-Allow-Headers: content-type, authorization
# Vary: OriginOr avoid CORS entirely
If the frontend and API are yours, serving both from one origin removes the problem: the API under /api in the same app, or a rewrite in the frontend’s server that forwards /api to the backend. Cookies get simpler too. Separate origins make sense when the API has other clients or a different release cycle. More guides are under hosting by framework.
Questions
Why does CORS work locally but not in production?
Locally the dev server usually proxies API calls, so the browser sees one origin. In production the frontend and API have different hostnames, and the API must allow the frontend’s exact origin, answer preflights with a 2xx, and avoid redirects.
Can I fix a CORS error from the frontend?
No. The API decides which origins may read its responses. mode: 'no-cors' only makes the response unreadable to your code. Fix the API’s headers or serve both from one origin.
Why does Access-Control-Allow-Origin: * fail with cookies?
Browsers refuse a wildcard on credentialed requests. Reflect the specific origin from an allowlist, send Access-Control-Allow-Credentials: true and add Vary: Origin.
Is a CORS error always a CORS problem?
No. A 500, a 404 or a gateway error without CORS headers shows up as a CORS error too. Check the status of the request in the Network tab and the API’s logs first.
Preview URLs you can put on an allowlist.
Every pull request gets a predictable branch--project.canner.app address, and CANNER_URL tells your code which one it is.