GuidesPYTHON
Fix CORS errors in FastAPI
Browsers withhold responses from your application when the server omits matching Access-Control headers. When you own the backend, configuring fastapi cors behavior through the Starlette CORSMiddleware supplies these headers and resolves the issue. If the target service is an external third-party API that you cannot modify, server-side middleware cannot help and an intermediary is required instead.
Confirm the fix belongs in FastAPI
A missing Access-Control-Allow-Origin header on responses from an API you control is a backend configuration task, not a client fetch bug. Follow the diagnosis guide to inspect the headers returned by your backend.
If the failing request points to a third-party API rather than your own FastAPI service, you cannot change its headers directly. A public proxy like cors.dev can re-serve public GET and HEAD responses from public HTTPS hosts with the headers your origin needs. The proxy operator can observe everything routed through it, so route public data only.
Nothing extra to install
CORSMiddleware ships with FastAPI through Starlette, so installing fastapi provides everything required. Import the middleware with from fastapi.middleware.cors import CORSMiddleware, a convenience re-export of starlette.middleware.cors.
Add CORSMiddleware
Register the middleware on the FastAPI application instance once. It applies globally across all registered routes.
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:5173"],
allow_methods=["GET", "POST"],
allow_headers=["Content-Type"],
)
@app.get("/api/users")
def list_users():
return {"users": ["ada", "grace"]}The default parameters are restrictive: allow_methods defaults to ['GET'] and allow_headers defaults to an empty list, so methods and custom request headers must be enabled explicitly. allow_credentials defaults to False and expose_headers defaults to an empty list.
List exact origins for production
Define allowed origins with full scheme, host, and port combinations in allow_origins.
origins = [
"https://app.example.com",
"https://admin.example.com",
]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
# Matches any subdomain of example.org over https
allow_origin_regex=r"https://.*\.example\.org",
allow_methods=["GET", "POST", "PUT", "DELETE"],
allow_headers=["Content-Type", "Authorization"],
)Setting allow_origins to ['*'] permits any origin to read the response but cannot serve credentialed requests. To match dynamic subdomains without permitting all origins, assign a regular expression to allow_origin_regex, such as https://.*\.example\.org.
How the middleware answers preflights
Any OPTIONS request carrying both Origin and Access-Control-Request-Method headers is intercepted as a preflight. The middleware answers the probe directly with a 200 status when the origin, method, and headers are permitted, or a 400 status with a message such as Disallowed CORS origin when they are not. Preflight traffic never executes your route handlers.
Simple requests bearing an Origin header pass through to your route handlers, and the middleware appends the configured Access-Control headers to the response. The Accept, Accept-Language, Content-Language, and Content-Type headers are always permitted for simple requests. The max_age setting defaults to 600 seconds, instructing browsers how long to cache the preflight answer.
Allow cookies and Authorization headers
To receive browser cookies, session identifiers, or Authorization headers, configure allow_credentials=True on the middleware and credentials: 'include' on the browser fetch call.
app.add_middleware(
CORSMiddleware,
allow_origins=["https://app.example.com"], # never ["*"] here
allow_credentials=True,
allow_methods=["GET", "POST"], # never ["*"] here
allow_headers=["Content-Type", "Authorization"],
)
# The browser fetch must opt in too, or cookies never leave the jar:
# fetch("https://api.example.com/api/users", { credentials: "include" })- Do not use
['*']forallow_originswhenallow_credentials=True. Browsers reject responses whereAccess-Control-Allow-Originis a wildcard if the request includes credentials. - Do not use
['*']forallow_methodswhenallow_credentials=True. List each allowed HTTP method explicitly. - Do not use
['*']forallow_headerswhenallow_credentials=True. List every expected request header explicitly.
Trailing-slash redirects look like CORS failures
Starlette answers a request to the slash twin of a registered route with a 307 Temporary Redirect: calling /items when the route is defined as /items/ returns a redirect to /items/.
Browsers reject redirects in answer to a preflight, and every redirect hop re-runs the CORS checks. A mismatched path therefore surfaces in the console as a CORS failure even with the middleware configured. Call the exact path registered in the route decorator, trailing slash included.
Verify the headers with curl
Use curl to simulate browser simple and preflight requests by supplying the Origin header directly.
$ curl -i -H "Origin: https://app.example.com" \
http://127.0.0.1:8000/api/users
HTTP/1.1 200 OK
access-control-allow-origin: https://app.example.com
$ curl -i -X OPTIONS \
-H "Origin: https://app.example.com" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: content-type" \
http://127.0.0.1:8000/api/users
HTTP/1.1 200 OK
access-control-allow-origin: https://app.example.com
access-control-allow-methods: GET, POST
access-control-allow-headers: Content-TypeA working configuration returns a 200 status on the OPTIONS request with Access-Control-Allow-Origin and Access-Control-Allow-Methods headers present. The subsequent GET request must also carry Access-Control-Allow-Origin matching the requested origin.
Good questions.
Does CORSMiddleware handle OPTIONS requests automatically?
Yes. When an incoming OPTIONS request includes both Origin and Access-Control-Request-Method headers, CORSMiddleware intercepts it, inspects your configuration, and responds directly with a 200 or 400 status without invoking your path operations.
Why does my FastAPI endpoint work in curl or Postman but fail in the browser?
Command-line tools and API clients do not enforce the same-origin policy. Browsers withhold responses from client-side scripts unless the server explicitly returns matching Access-Control-Allow-Origin headers.
Why did allow_origins=['*'] fail when allow_credentials=True was enabled?
The CORS protocol prohibits wildcards when credentials are included. When allow_credentials=True, none of allow_origins, allow_methods, or allow_headers may be ['*']; all of them must be stated explicitly.
Why does a 307 redirect show up as a CORS error in the browser?
Starlette sends a 307 Temporary Redirect when an endpoint path is missing or unexpectedly includes a trailing slash. Browsers do not follow redirects on preflight OPTIONS checks, so the browser aborts the request and logs a CORS failure.