FastAPI question
FastAPI on Vercel: CORS error, "Access-Control-Allow-Origin" header missing
"Reason: CORS header 'Access-Control-Allow-Origin' missing" means the response from your FastAPI app on Vercel had no Access-Control-Allow-Origin header, so the browser hid it from your Vite app. Three causes reproduce locally: no CORSMiddleware, so the preflight OPTIONS answers 405; an allow_origins entry that is not the exact origin, such as one with a trailing slash, so the preflight answers 400 with no header; or an unhandled exception, whose 500 carries no CORS header and hides the real crash. On preview deployments Vercel can also answer the preflight with 401 before FastAPI runs; add the path to the OPTIONS Allowlist. Reproduced with fastapi 0.162.0 and starlette 1.10.0 on 2026-10-31.
Why does the preflight fail when a plain GET works?
A browser sends a preflight OPTIONS request first when a call uses a method or headers that a simple request does not, for example a POST with an Authorization header. FastAPI's CORSMiddleware answers it, with a 200 or a 400 "for informational purposes" per the docs, and its defaults are restrictive: allow_methods is GET and allow_headers is empty. The same preflight (Origin https://my-app.vercel.app, POST, Authorization) gave these answers in a run with fastapi 0.162.0 and starlette 1.10.0.
no middleware 405 Method Not Allowed (no header)
allow_origins=["http://localhost:5173"] 400 Disallowed CORS origin, method, headers (no header)
allow_origins=["https://my-app.vercel.app/"] 400 Disallowed CORS origin (no header)
right origin, default methods and headers 400 Disallowed CORS method, headers (header present)
right origin, allow_methods and allow_headers ["*"] 200 OK (header present)Why does the console say CORS when the real problem is a 500?
Starlette's docs call ServerErrorMiddleware always the outermost layer, so the 500 for an unhandled exception is built outside CORSMiddleware. In the run, a route that raised RuntimeError answered 500 with no Access-Control-Allow-Origin header, while a normal GET from the same origin had it. The browser reports the missing header, not the exception. A function that fails on import, for example because of a package missing from the dependency file, behaves the same way. Starlette's fix is to wrap the whole app; in the same run, wrapping gave a 500 that carried the header. Read the Runtime Logs for the cause.
api = FastAPI()
app = CORSMiddleware(
api,
allow_origins=["https://my-app.vercel.app"],
allow_methods=["*"],
allow_headers=["*"],
)The preflight returns 401 on a Vercel preview deployment
Vercel's CORS guide says an unauthenticated preflight OPTIONS request gets a 401 from Vercel before your middleware or function runs when Deployment Protection is on. FastAPI never sees the request, so no CORSMiddleware change helps. The OPTIONS Allowlist, available on all plans, turns protection off for OPTIONS requests on paths that start with the path you list, for example /api. A Vercel Community thread from 2025-07-02 shows a React + Vite frontend and a FastAPI backend with exactly this: "Preflight response is not successful. Status code: 401", and a reply in the thread says a protection bypass header did not help, because the 401 comes before the app runs.
Checklist
How to fix it, then check the fix
- 01Send the preflight yourself and read the status, from the Vercel CORS guide:
curl -i -X OPTIONS https://my-api.vercel.app/api/items \ -H "Origin: https://my-app.vercel.app" \ -H "Access-Control-Request-Method: POST" - 02401 means Vercel answered, so add the path to the OPTIONS Allowlist under Deployment Protection. 405 means FastAPI has no CORSMiddleware. 400 means it rejected the origin, method or headers. 200 with Access-Control-Allow-Origin is the goal.
- 03Add CORSMiddleware with exact origins: scheme, domain and port, with no trailing slash. Read them from a variable so Production and Preview can differ:
import os from fastapi.middleware.cors import CORSMiddleware origins = [o.strip() for o in os.environ.get("ALLOWED_ORIGINS", "http://localhost:5173").split(",") if o.strip()] app.add_middleware(CORSMiddleware, allow_origins=origins, allow_methods=["*"], allow_headers=["*"]) - 04Set ALLOWED_ORIGINS in Vercel for Production and Preview. Vercel applies changed variables only to new deployments, so redeploy. For preview URLs that change per branch, allow_origin_regex takes a regex you can match to your own preview URL.
- 05If a request still shows a CORS error, open the Network tab and the Runtime Logs: a 500 on the real request means fix the crash first, then the header follows.
Related
Related guides
Sources
Official docs behind this checklist
- FastAPI, CORS (Cross-Origin Resource Sharing)
- Starlette, Middleware (CORSMiddleware)
- Vercel, OPTIONS Allowlist
- Vercel, How to enable CORS on Vercel
- Vercel, Environment variables
- MDN, Reason: CORS header Access-Control-Allow-Origin missing
- Vercel Community, CORS Preflight Request fails with 401 in Preview Deployment
Browse all launch guides · Read resources · How to make and check your next change
FAQ
FAQ for builders before launch
Can I use allow_origins=["*"] with allow_credentials=True?
The FastAPI docs say none of allow_origins, allow_methods and allow_headers can be ["*"] when allow_credentials is True; they must be listed. In the run, Starlette answered with the request's own origin in that setup, so it looked like it worked, but list the origins explicitly.
Should CORS be set in vercel.json or in FastAPI?
Vercel's guide says headers in vercel.json apply at the CDN before the function and take only static values, so they cannot check an origin against an allowlist. CORSMiddleware checks it in your code.
Is a plain GET with no preflight affected too?
Yes. The FastAPI docs say the middleware adds CORS headers to the response of any request with an Origin header. With no CORSMiddleware, the run answered a cross-origin GET with 200 and no Access-Control-Allow-Origin header, so the server did its work and the browser still hid the result.