What this usually means
CORS (Cross-Origin Resource Sharing) is a browser security mechanism that restricts web pages from making requests to a different origin than the one that served the page. When a frontend hosted on e.g. http://localhost:3000 tries to call a FastAPI backend on http://localhost:8000, the browser sends a preflight OPTIONS request first (for non-simple requests) and expects the backend to respond with specific CORS headers. If those headers are missing, incorrect, or the preflight fails, the browser blocks the actual request. The typical root cause is misconfigured CORSMiddleware in FastAPI: wrong allowed origins, missing methods or headers, or not handling OPTIONS requests correctly.
The first ten minutes — establish facts before touching code.
- 1Open browser DevTools Network tab, reproduce the error, and inspect the failed request (usually shows CORS error). Check if there is an OPTIONS preflight request and its status.
- 2Identify the exact origin, method, and headers the frontend sends (e.g., Origin: http://localhost:3000, Authorization header).
- 3Check the FastAPI backend logs for any errors or missing routes (OPTIONS /endpoint).
- 4Verify the CORSMiddleware configuration in your FastAPI app: ensure add_middleware is called with correct parameters.
- 5Test the endpoint with curl including an Origin header: curl -H 'Origin: http://localhost:3000' -H 'Access-Control-Request-Method: POST' -X OPTIONS http://localhost:8000/endpoint -v
The specific files, logs, configs, and dashboards that usually own this bug.
- searchFastAPI app.py or main.py – the file where CORSMiddleware is added
- searchBrowser DevTools Network tab – inspect request/response headers for CORS headers
- searchBackend server logs (stdout/stderr) – check for errors or unhandled OPTIONS requests
- searchFrontend code – the fetch or axios call, verify the URL is correct (no trailing slash mismatch)
- searchDocker or reverse proxy configuration (nginx, traefik) – may strip or modify headers
- searchEnvironment variables – ensure CORS_ORIGINS list is correctly parsed
- searchFastAPI documentation on CORSMiddleware – verify parameter order and types
Practical causes, not theory. These are the things you will actually find.
- warningCORSMiddleware not added or added with incorrect parameters (e.g., allow_origins=['*'] but allow_credentials=True)
- warningMissing allowed methods or headers in middleware config (especially for preflight)
- warningUsing allow_origins=['*'] with credentials (cookies, Authorization) – browsers reject this
- warningBackend not responding to OPTIONS requests (preflight) – returns 405 Method Not Allowed
- warningFrontend sending non-standard headers (e.g., X-Custom-Header) not in allowed_headers
- warningProxy (nginx, Caddy) stripping or not forwarding CORS headers
- warningFastAPI middleware order – CORSMiddleware must be added before other middleware that may short-circuit requests
Concrete fix directions. Pick the one that matches your root cause.
- buildAdd CORSMiddleware with explicit allowed origins, methods, and headers: origins = ['http://localhost:3000']
- buildIf credentials are needed (cookies, auth headers), set allow_credentials=True and avoid wildcard origins
- buildEnsure middleware is added before any other middleware that might intercept requests (e.g., AuthenticationMiddleware)
- buildFor production, read allowed origins from environment variable: origins = os.getenv('CORS_ORIGINS', '').split(',')
- buildAdd explicit OPTIONS route handler if middleware doesn't handle it (unlikely but possible): @app.options('/{path:path}')
- buildIf behind a proxy, configure the proxy to forward CORS headers or let FastAPI handle it (set CORSMiddleware after proxy middleware)
- buildUse starlette.middleware.cors.CORSMiddleware directly if needed (same as FastAPI's)
A fix you cannot prove is a guess. Close the loop.
- verifiedAfter fix, reload frontend and make the request – should succeed without CORS error
- verifiedUse curl to send an OPTIONS request with Origin header and verify response has Access-Control-Allow-Origin matching origin
- verifiedCheck response headers of the actual request (GET/POST) – they should include Access-Control-Allow-Origin
- verifiedRun automated tests that simulate cross-origin requests (e.g., with pytest and httpx using custom headers)
- verifiedMonitor backend logs for any future CORS-related errors
- verifiedTest with different browsers (Chrome, Firefox) to ensure consistency
Things that make this bug worse or harder to find.
- warningSetting allow_origins=['*'] with allow_credentials=True – browser will reject; must use explicit origins
- warningForgetting to include the exact origin including port (e.g., 'http://localhost:3000' not 'localhost:3000')
- warningAdding CORSMiddleware after other middleware that returns early (e.g., trusted host middleware)
- warningHardcoding origins in code – makes deployment inflexible
- warningAssuming OPTIONS request is handled automatically – verify it is (it usually is, but can be blocked)
- warningUsing allow_origins with a trailing slash (e.g., 'http://example.com/') – browsers treat it as different origin
Frontend blocked by CORS after adding authentication headers
Timeline
- 09:00Deploy new authentication feature: frontend sends Authorization header
- 09:15User reports login fails; console shows CORS error
- 09:20Check network tab: OPTIONS request returns 405 Method Not Allowed
- 09:25Check FastAPI code: CORSMiddleware added but allow_methods missing 'OPTIONS' (default includes it though)
- 09:30Test with curl: OPTIONS fails with 405; GET without auth header works
- 09:35Realize that a custom middleware 'CustomAuthMiddleware' runs before CORSMiddleware and blocks OPTIONS
- 09:40Reorder middleware: move CORSMiddleware before CustomAuthMiddleware
- 09:45Deploy fix; login successful; verify network tab shows proper CORS headers
I had just deployed a new authentication feature that required the frontend to send an Authorization header. Almost immediately, users reported being unable to log in. The browser console showed a CORS error: 'Access to fetch at ... from origin ... has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.' I was confused because CORS had been working fine before.
Opening DevTools, I saw that the failed request was a POST to /login. There was also a preceding OPTIONS request that returned 405 Method Not Allowed. This was the preflight – the browser sends an OPTIONS to check if the server allows the actual request. Since the server returned 405, the browser blocked the POST. I checked my FastAPI code and found CORSMiddleware added correctly, but I had also added a custom middleware for authentication that was intercepting all requests, including OPTIONS.
The fix was simple: I moved the CORSMiddleware addition before the custom middleware in the app. This ensured that OPTIONS requests were handled by CORSMiddleware first, returning the proper CORS headers. After redeploying, the login worked. The lesson: middleware order matters. CORSMiddleware should be as early as possible in the middleware stack to avoid interference.
Root cause
Custom middleware added before CORSMiddleware intercepted OPTIONS preflight requests, returning 405 instead of proper CORS headers.
The fix
Reordered middleware: added CORSMiddleware before any custom middleware that might block or short-circuit requests.
The lesson
Always add CORSMiddleware as the first middleware in FastAPI to ensure preflight requests are handled correctly before any other middleware processes them.
CORS is enforced by the browser, not the server. When a frontend (e.g., http://localhost:3000) makes a cross-origin request to a backend (http://localhost:8000), the browser adds an Origin header. For requests that are not 'simple' (e.g., with custom headers, non-GET methods, or credentials), the browser first sends an OPTIONS preflight request to check if the server permits the actual request.
The server must respond to the OPTIONS request with appropriate CORS headers: Access-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Allow-Headers. If any of these are missing or mismatch the request, the browser blocks the actual request. A common failure is the server returning 405 for OPTIONS because no route handles it, or middleware rejecting it.
The correct way to add CORS support in FastAPI is via CORSMiddleware from starlette.middleware.cors (already included in FastAPI). You call app.add_middleware(CORSMiddleware, allow_origins=[...], allow_methods=[...], allow_headers=[...], allow_credentials=True/False). The order of parameters matters: the first positional arg after CORSMiddleware is the middleware class, then keyword args.
Beware of the wildcard '*' with credentials: if you set allow_origins=['*'] and allow_credentials=True, the browser will reject it. You must explicitly list origins. Also, allow_methods defaults to ['GET'], but you typically want to include all relevant methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS']. Similarly, allow_headers should include any custom headers your frontend sends, like 'Authorization', 'Content-Type', etc.
When FastAPI runs behind a reverse proxy like nginx or traefik, the proxy may interfere with CORS headers. For instance, nginx can strip the Origin header or not forward OPTIONS requests. Check your proxy configuration to ensure it passes through the Origin header and does not modify the response headers. Alternatively, you can configure CORS at the proxy level (e.g., using nginx's add_header directives) and disable it in FastAPI.
Another common issue is the proxy terminating TLS and forwarding to HTTP, causing the backend to see a different origin (e.g., https://example.com vs http://internal). Ensure your FastAPI app knows its real origin via the Host header or by setting the root_path if behind a path prefix.
To test CORS without a browser, you can simulate preflight with curl: curl -H 'Origin: http://localhost:3000' -H 'Access-Control-Request-Method: POST' -H 'Access-Control-Request-Headers: Authorization' -X OPTIONS http://localhost:8000/endpoint -v. Check the response headers for Access-Control-Allow-Origin etc. If the server returns 405, there's a routing issue.
You can also write automated tests using httpx or requests with custom headers: response = client.options('/endpoint', headers={'Origin': 'http://localhost:3000', 'Access-Control-Request-Method': 'POST'}). Assert that response.status_code is 200 and that the CORS headers are present.
A frequent mistake is setting allow_origins=['*'] and allow_credentials=True. The CORS spec explicitly forbids this combination because it would allow any site to make authenticated requests. The browser will reject the response. To fix, you must list the specific origins that are allowed to send credentials (cookies, auth headers).
If you have multiple origins (e.g., staging and production), you can read them from an environment variable as a comma-separated list and split. For dynamic origins (e.g., subdomains), you may need to use a custom origin validator function instead of a list.
Frequently asked questions
Why does my FastAPI CORS work with curl but not in the browser?
Curl does not enforce CORS; it simply sends requests and shows responses. The browser is the one that blocks requests based on CORS policy. If curl works, it means the server is responding correctly, but the browser may be missing certain headers (e.g., Access-Control-Allow-Origin) or the preflight check fails. Check the actual response headers in the browser's network tab and compare with curl's verbose output.
What is the difference between allow_origins and allow_origin_regex?
allow_origins is a list of exact origin strings (e.g., ['http://localhost:3000']). allow_origin_regex allows you to specify a regex pattern to match origins (e.g., 'https://.*\.example\.com'). Use regex when you have dynamic origins like subdomains. Note that if you use allow_origins with credentials, you cannot use wildcards; similarly, regex must match exactly one origin per request.
How do I handle CORS for multiple frontend domains?
You can set allow_origins to a list of all allowed origins, e.g., ['http://localhost:3000', 'https://app.example.com', 'https://staging.example.com']. For many domains, consider using allow_origin_regex with a pattern that matches them. Alternatively, implement a custom middleware that checks the Origin header against a list or database.
My preflight OPTIONS request returns 400 Bad Request, what's wrong?
A 400 on OPTIONS often means the server cannot parse the request. This could be due to malformed headers or a middleware that rejects the OPTIONS method. Check if you have any middleware that validates Content-Type or other headers before CORSMiddleware runs. Also ensure that your FastAPI app is not behind a proxy that modifies the request in unexpected ways.
Can I disable CORS entirely for development?
Yes, for development you can set allow_origins=['*'] and allow_credentials=False. However, never use this in production as it exposes your API to any website. A better approach for development is to use a proxy in your frontend dev server (e.g., CRA proxy or Vite proxy) to avoid CORS altogether.