FastAPI CORS error on localhost (tested)
A FastAPI CORS error from a local frontend usually comes from one of four things we reproduced: no CORSMiddleware (preflight gets 405), an origin with a trailing slash or 127.0.0.1 instead of localhost (400 Disallowed CORS origin), or the default allow_methods, which only allows GET.
Each case sends the preflight a browser sends before a JSON POST from http://localhost:3000.
1. Six configurations, one preflight
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.testclient import TestClient
pre = {"Origin": "http://localhost:3000", "Access-Control-Request-Method": "POST",
"Access-Control-Request-Headers": "content-type"}
def make(**cors):
app = FastAPI()
if cors: app.add_middleware(CORSMiddleware, **cors)
@app.post("/items")
def items(): return {"ok": True}
return TestClient(app)
def show(label, c):
r = c.options("/items", headers=pre)
print(f"--- {label}: preflight {r.status_code}, allow-origin={r.headers.get('access-control-allow-origin')}, body={r.text[:40]!r}")
show("no CORSMiddleware", make())
show("allow_origins=['http://localhost:3000/'] (trailing slash)", make(allow_origins=["http://localhost:3000/"], allow_methods=["*"], allow_headers=["*"]))
show("allow_origins=['http://127.0.0.1:3000']", make(allow_origins=["http://127.0.0.1:3000"], allow_methods=["*"], allow_headers=["*"]))
show("allow_origins=['http://localhost:3000'], default methods", make(allow_origins=["http://localhost:3000"]))
show("allow_origins=['http://localhost:3000'], methods/headers *", make(allow_origins=["http://localhost:3000"], allow_methods=["*"], allow_headers=["*"]))
show("allow_origins=['*'] + allow_credentials=True", make(allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"]))
Output:
--- no CORSMiddleware: preflight 405, allow-origin=None, body='{"detail":"Method Not Allowed"}'
--- allow_origins=['http://localhost:3000/'] (trailing slash): preflight 400, allow-origin=None, body='Disallowed CORS origin'
--- allow_origins=['http://127.0.0.1:3000']: preflight 400, allow-origin=None, body='Disallowed CORS origin'
--- allow_origins=['http://localhost:3000'], default methods: preflight 400, allow-origin=http://localhost:3000, body='Disallowed CORS method'
--- allow_origins=['http://localhost:3000'], methods/headers *: preflight 200, allow-origin=http://localhost:3000, body='OK'
--- allow_origins=['*'] + allow_credentials=True: preflight 200, allow-origin=http://localhost:3000, body='OK'
What each result means:
- No CORSMiddleware: the
OPTIONSpreflight hits your route and gets 405 Method Not Allowed. That 405 in your server log is a CORS problem, not a routing one. - Trailing slash (
http://localhost:3000/) or 127.0.0.1 vs localhost: origins are compared as exact strings (scheme, host, port, no path), so these give 400 Disallowed CORS origin. - Origin right, default
allow_methods: the default allows only GET, so a POST preflight gets 400 Disallowed CORS method. Passallow_methods=["*"](andallow_headers=["*"]forContent-Type: application/jsonorAuthorization). ["*"]withallow_credentials=True: Starlette 1.9.0 echoes the request's origin instead of*, so cookies work, but any site can then make credentialed requests. List real origins in production.
Working setup for local development
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000", "http://127.0.0.1:3000"],
allow_methods=["*"],
allow_headers=["*"],
allow_credentials=True, # only if you send cookies
)
That is the passing configuration from block 1 with the second local origin added. If the browser still reports CORS, check the actual response in the Network tab: a 500 from your endpoint can also arrive without CORS headers, and the browser reports it as a CORS error.
Related: CORS error messages explained · CORS preflight (OPTIONS) · FastAPI 422.
Frequently asked questions
Why does FastAPI return 405 for OPTIONS requests?
No CORSMiddleware is installed, so the browser's preflight OPTIONS request reaches your route, which has no OPTIONS handler.
Why do I get Disallowed CORS origin from FastAPI?
The Origin header does not exactly match an entry in allow_origins. Remove trailing slashes and list both http://localhost:PORT and http://127.0.0.1:PORT if you use both.
Why do I get Disallowed CORS method?
CORSMiddleware's default allow_methods is GET only. Pass allow_methods=['*'] or list POST, PUT, DELETE.
Written with AI assistance. Measured on 2026-10-23 with FastAPI 0.161.1, Starlette 1.9.0 and Python 3.14.7 (TestClient, python:3-alpine in Docker). Each block was run verbatim and the output pasted unedited. PlainHeaders is our extension; Python and FastAPI are free.