Middleware in FastAPI intercepts every HTTP request and response.
Use app.add_middleware() for global configurations like CORS or GZip compression.
For custom logic, use @app.middleware('http') decorator to wrap call_next.
Execution follows an 'Onion' pattern: last added runs first for requests, last for responses.
Each middleware adds ~0.1-1ms overhead; 20+ layers can add noticeable latency.
CORS errors in production are often caused by middleware ordering or missing allowed headers.
✦ Definition~90s read
What is FastAPI Middleware?
FastAPI middleware sits in the request/response pipeline, intercepting every HTTP call before it reaches your route handlers and again on the way out. Think of it as a series of hooks that let you inspect, modify, or block traffic without cluttering your business logic.
★
Think of FastAPI middleware like security checkpoints at an airport: every passenger (request) must pass through them before reaching the gate (your route handler), and they go through the same checkpoints on the way out.
The most common use case is CORS middleware, which handles cross-origin requests by injecting the right headers — FastAPI wraps Starlette's CORSMiddleware, and you configure origins, methods, and headers declaratively. But middleware goes far beyond CORS: you can build custom logging that measures response times per endpoint, implement IP-based blocking by short-circuiting requests early, or catch unhandled exceptions globally to return consistent error JSON instead of 500 tracebacks.
Middleware executes in the order you add it, following an onion model: each layer wraps the next, so the first middleware added is the outermost layer, processing requests first and responses last. This matters when you short-circuit — if you block an IP in an early middleware, later middleware and the route handler never run.
For performance logging, you'd typically place it early to capture timing for all requests, including those blocked later. Error handling middleware should be outermost to catch exceptions from all inner layers, including other middleware. FastAPI's ASGI foundation means middleware can also be async, letting you await database calls or external APIs without blocking the event loop.
In practice, you'll reach for middleware when you need cross-cutting concerns that apply to every request — authentication checks, rate limiting, request ID injection, or response compression. But don't overuse it: middleware adds latency to every request, and complex logic here can obscure debugging.
For one-off route-specific behavior, dependency injection or route decorators are cleaner. The sweet spot is infrastructure-level concerns that would otherwise require repetitive code in every handler. FastAPI's middleware API is minimal — just a class with __init__ and __call__ — but it gives you full control over the ASGI scope, receive, and send channels, which is both powerful and dangerous if you mutate state carelessly.
Plain-English First
Think of FastAPI middleware like security checkpoints at an airport: every passenger (request) must pass through them before reaching the gate (your route handler), and they go through the same checkpoints on the way out. Each checkpoint can inspect bags, add a boarding pass stamp, or turn someone away entirely—without the gate agent ever knowing it happened. The order of these checkpoints matters because once someone is turned away at an early checkpoint, later ones never get a chance to run.
In production at TheCodeForge, we treat middleware as the 'Defensive Perimeter' of our services. Middleware handles cross-cutting concerns—logic that shouldn't clutter your business endpoints. Whether you're enforcing Cross-Origin Resource Sharing (CORS) policies, injecting global trace IDs for distributed logging, or measuring request latency, middleware provides a centralized point of control that wraps your entire ASGI application.
But here's the thing: one wrong middleware can silently drop all requests. That's not theory—it's a 2 AM pager call. Understanding execution order, short-circuiting behavior, and how to avoid blocking the event loop is what separates a working API from one that crashes under load.
This article covers the patterns we use, the production traps we've fixed, and the debugging steps that get you back online fast.
How FastAPI CORS Middleware Actually Works
CORS middleware in FastAPI intercepts every incoming HTTP request before it reaches your route handler and adds the necessary CORS headers to the response. It's a WSGI/ASGI middleware that wraps the entire application, inspecting the Origin header and deciding whether to include Access-Control-Allow-Origin, Access-Control-Allow-Methods, and Access-Control-Allow-Headers based on your configuration. The core mechanic is simple: it's a preflight handler for OPTIONS requests and a response header injector for all other requests.
In practice, FastAPI's CORSMiddleware operates at the ASGI level, meaning it runs before any path operation decorator or dependency injection. It matches origins against a whitelist using exact string comparison or regex patterns. The middleware caches the allowed origins in memory, so lookups are O(1) after initialization. A critical detail: if you set allow_origins=["*"], the middleware will echo back the request's Origin header — it does not blindly send a wildcard when credentials are involved, because the CORS spec forbids wildcard with credentials.
You should use FastAPI's CORSMiddleware when your API serves a browser-based frontend on a different domain, port, or protocol. Without it, browsers will block cross-origin requests entirely. In production, never use allow_origins=["*"] with allow_credentials=True — the browser will reject the response. Instead, explicitly list your frontend domains. This middleware is also the correct place to handle preflight caching via Access-Control-Max-Age to reduce OPTIONS request volume.
⚠ Wildcard + Credentials = Silent Failure
Setting allow_origins=["*"] with allow_credentials=True causes the browser to reject the response — the middleware sends a wildcard, which violates the CORS spec when credentials are present.
📊 Production Insight
A payment dashboard failed to load user transactions after a frontend domain change — the old origin was still in the whitelist, so the new domain got no Access-Control-Allow-Origin header.
The browser console showed 'Access to fetch at ... from origin ... has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.'
Always maintain a separate, version-controlled CORS configuration file and test with a preflight OPTIONS request after every deployment.
🎯 Key Takeaway
CORS middleware runs before your route handlers — it's not part of your application logic.
Never use wildcard origins with credentials; always enumerate allowed origins in production.
Preflight caching via Access-Control-Max-Age is essential to reduce OPTIONS request overhead on high-traffic APIs.
thecodeforge.io
Fastapi Middleware Cors
CORS Middleware: Securing Cross-Origin Traffic
CORS is a security feature, not an error. When your frontend (e.g., React on port 3000) tries to talk to your FastAPI backend (port 8000), the browser blocks the request unless the server explicitly permits it. For production, never use ['*']. Always whitelist specific, trusted domains.
But it gets tricky: when allow_credentials=True, the Access-Control-Allow-Origin response header must be a single origin, not a wildcard. The browser enforces this. And if you have a middleware that returns a 403 for unauthenticated requests before CORS headers are set, the frontend gets a CORS error—not a 403. That's a common head-scratcher.
io/thecodeforge/middleware/security.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from fastapi importFastAPIfrom fastapi.middleware.cors importCORSMiddleware
app = FastAPI()
# Configure the 'Onion' layers# CORSMiddleware should generally be added early in the stack
app.add_middleware(
CORSMiddleware,
# List specific trusted origins for production
allow_origins=[
'https://api.thecodeforge.io',
'https://dashboard.thecodeforge.io',
'http://localhost:3000'
],
# Required if your frontend sends cookies or Authorization headers
allow_credentials=True,
allow_methods=['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allow_headers=['Authorization', 'X-Forge-Trace-ID', 'Content-Type'],
)
# Note: If allow_credentials is True, allow_origins cannot be ['*']
If you set allow_credentials=True and allow_origins=['*'], modern browsers will reject the preflight response. The spec requires an explicit origin when credentials are involved.
Always use explicit origins in production.
For development, list http://localhost:3000 explicitly; don't use * even locally if you need cookies.
📊 Production Insight
CORS middleware is order-sensitive. If an earlier middleware (like an auth check) returns an error without the CORS headers, the browser blocks the error response.
Solution: add CORSMiddleware first in the stack (last in the list of add_middleware calls).
Rule: the first middleware to handle requests should set CORS headers on every response.
🎯 Key Takeaway
CORS is not about allowing everything. It's about telling the browser which sites are trusted.
Whitelist explicit origins. Never pair wildcard with credentials.
If you see a CORS error in production, check middleware order first.
UseUse allow_origins=[specific_domain] with allow_credentials=True
IfPublic API, no cookies
→
UseWildcard ['*'] is acceptable, but still prefer explicit list for logging
IfMultiple domains, need credentials
→
UseImplement dynamic origin per request using a custom middleware that checks origin against a whitelist
Custom Middleware — Performance Logging
Custom middleware uses the call_next pattern. This allows you to run code before the request reaches your route and after the response has been generated. This is the ideal place to calculate 'Time to First Byte' (TTFB) or inject unique request identifiers for log aggregation.
But there's a subtle trap: if you do heavy synchronous work (like hashing or JSON serialization) in the middleware, you block the entire event loop. All concurrent requests wait. Always push heavy work to a thread pool or make it async-friendly.
io/thecodeforge/middleware/logging.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
from fastapi importFastAPI, Requestimport time
import uuid
import logging
app = FastAPI()
logger = logging.getLogger("thecodeforge.access")
@app.middleware('http')
asyncdefadd_process_time_header(request: Request, call_next):
# 1. Logic BEFORE the route (Request Phase)
start_time = time.perf_counter()
request_id = str(uuid.uuid4())
# Inject trace ID into request state for downstream access
request.state.trace_id = request_id
# 2. Hand off to the next middleware or route handler
response = awaitcall_next(request)
# 3. Logic AFTER the route (Response Phase)
process_time = time.perf_counter() - start_time
# Log the performance metric
logger.info(f"RID: {request_id} | Path: {request.url.path} | Time: {process_time:.4f}s")
# Standardize our response headers
response.headers['X-Forge-Process-Time'] = str(process_time)
response.headers['X-Forge-Trace-ID'] = request_id
return response
Imaging each middleware as a layer of an onion. The request travels inward through layers, and the response travels outward.
The middleware added last wraps the outermost layer. It sees the request first and the response last.
Each middleware calls call_next to hand off to the next inner layer.
Code before await call_next(request) runs during the request phase (inward).
Code after await call_next(request) runs during the response phase (outward).
Short-circuiting means a middleware returns a response without calling call_next, cutting the onion.
📊 Production Insight
If a middleware performs a blocking I/O operation (e.g., time.sleep(0.5)) in the async path, the entire event loop stalls.
Solution: use asyncio.to_thread for blocking calls, or shift heavy work to a background task.
Rule: keep middleware logic lightweight and non-blocking.
🎯 Key Takeaway
Measure performance, but don't degrade performance.
Use time.perf_counter() not time.time() for nanosecond precision.
Never block the event loop in middleware. If it blocks, isolate it.
thecodeforge.io
Fastapi Middleware Cors
Understanding Middleware Execution Order: The Onion Model
FastAPI middleware follows a Last-In-First-Out (LIFO) order for requests and First-In-First-Out (FIFO) for responses. The last middleware you add with app.add_middleware() is the first to process the request, and the last to process the response. This is identical to how ASP.NET Core and Express middleware work.
This matters because of the onion model: if middleware A adds CORS headers and middleware B injects a trace ID, middleware B must be added before A (so B runs first on the request) to ensure the trace ID is available before CORS processing. But then CORS will run last on the response, meaning the trace ID header might not be visible to the frontend if CORS strips unknown headers. You need to explicitly allow X-Forge-Trace-ID in allow_headers.
Think of middleware processing as a stack for requests and a queue for responses.
Requests travel through middleware in stack order (LIFO).
Add middleware A first, then B. Request hits B first, then A.
Response travels through in queue order (FIFO).
So B sees the response first, then A.
If you want something to run after everything, add it last (innermost).
📊 Production Insight
A common production mistake is adding a security header middleware after CORS, assuming the headers will be visible to the browser. But CORS runs after on the response, potentially overwriting headers.
Fix: add security header middleware after CORS (i.e., add it before CORS in code) so it runs after CORS on the response.
Rule: trace your middleware order: print a log entry on entry and exit during investigation.
🎯 Key Takeaway
Middleware order is not just for execution—it's for header visibility too.
Add CORS last in code so it wraps the entire response.
Log entry/exit of each middleware when debugging order issues.
Short-Circuiting Middleware: Early Returns and IP Blocking
Sometimes you don't want to forward a request at all—like when an IP is on a blocklist, or you need to rate-limit a client. In FastAPI middleware, you can short-circuit by returning a Response object directly without calling call_next. This skips the rest of the middleware chain and the route handler, sending the response straight back to the client.
But there's a catch: if you short-circuit, you must ensure that any mandatory middleware (like CORS) hasn't been skipped. Because of the onion order, if you short-circuit in an outer middleware, no inner middleware runs. That means no CORS headers on the block response, and the client may not see the 403—just a CORS error. The solution: add CORS middleware as the outermost (first added) so it always runs, even on short-circuited responses.
io/thecodeforge/middleware/ip_blocker.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from fastapi importFastAPI, Request, Responseimport logging
app = FastAPI()
logger = logging.getLogger("thecodeforge.security")
BLOCKED_IPS = {"10.0.0.99", "192.168.1.200"}
@app.middleware("http")
asyncdefip_blocker(request: Request, call_next):
client_ip = request.client.host if request.client else"unknown"if client_ip in BLOCKED_IPS:
logger.warning(f"Blocked IP: {client_ip}")
returnResponse(
content='{"detail": "Forbidden"}',
status_code=403,
media_type="application/json"
)
# If not blocked, continue
response = awaitcall_next(request)
return response
⚠ Short-Circuiting and CORS
If your IP blocker short-circuits before CORSMiddleware (because it was added after CORSMiddleware in code), the block response will not include CORS headers. The browser sees a CORS error, not the 403.
Solution: add the IP blocker middleware after CORSMiddleware (i.e., app.add_middleware(IPBlockerMiddleware, ...) after app.add_middleware(CORSMiddleware, ...)) so that CORSMiddleware wraps the IP blocker and adds headers to its responses.
📊 Production Insight
Short-circuiting middlewares are ideal for early exits, but they break the onion. Every short-circuit means downstream middlewares never execute.
Impact: metrics middleware won't log the blocked request, trace IDs won't be injected, etc.
Mitigation: move metrics to a middleware that runs before IP blocking (i.e., add it after in code).
Rule: design your middleware stack so that the outermost (first added) layers are those that must always execute.
🎯 Key Takeaway
Short-circuiting is powerful but dangerous.
Ensure mandatory middlewares (CORS, logging) run before short-circuiting middlewares.
Test short-circuited paths for missing headers.
Error Handling Middleware: Global Exception Catching
FastAPI provides exception handlers, but a middleware can also catch exceptions that bubble up from routes or other middlewares. By wrapping await call_next(request) in a try-except, you can catch any unhandled exception and return a consistent JSON error response. This is especially useful for catching ValidationError, HTTPException, or unexpected server errors.
But careful catching everything can mask bugs. We log the error and return a 500 with a generic message, but preserve the trace ID for correlation.
io/thecodeforge/middleware/error_handler.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from fastapi importFastAPI, Request, Responsefrom fastapi.responses importJSONResponseimport logging
import traceback
thecodeforge_logger = logging.getLogger("thecodeforge.error")
app = FastAPI()
@app.middleware("http")
asyncdefcatch_exceptions_middleware(request: Request, call_next):
try:
response = awaitcall_next(request)
return response
exceptExceptionas exc:
# Log the full traceback internally
thecodeforge_logger.error(
f"Unhandled exception on {request.method} {request.url.path}: {traceback.format_exc()}"
)
# Return a generic error to the clientreturnJSONResponse(
status_code=500,
content={"detail": "Internal server error. Please try again later.",
"trace_id": request.state.trace_id ifhasattr(request, 'state') elseNone}
)
🔥Exception Handling in Middleware vs. FastAPI Exception Handlers
FastAPI's @app.exception_handler catches exceptions after middleware has already processed them. A middleware try-except around call_next catches exceptions during middleware processing. Use both, but be aware that if your middleware catches an exception and returns a response, the FastAPI exception handler will not run.
📊 Production Insight
If you catch all exceptions in middleware and return a 500, you may lose HTTPException errors (e.g., 404, 422) that FastAPI would normally handle gracefully.
Solution: re-raise HTTPException after logging, or check the exception type.
Rule: let HTTPException pass through to FastAPI's handler; catch only Exception for truly unexpected errors.
🎯 Key Takeaway
Error handling middleware is a safety net, not a replacement for specific exception handlers.
Always log the full traceback. Always include a trace ID in the error response.
Don't swallow HTTPException—let FastAPI handle it correctly.
Testing Middleware with FastAPI TestClient
You can't ship middleware without tests. FastAPI's TestClient provides a way to simulate requests and inspect responses without running the server. You can test that headers are set, that short-circuiting works, and that logging is called.
But a common mistake: the TestClient doesn't run ASGI middleware in the exact same way as a production server. Some middleware that relies on request.client.host or advanced ASGI features may behave differently in tests. We'll show you how to mock them and what to watch out for.
io/thecodeforge/tests/test_middleware.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import pytest
from fastapi.testclient importTestClient
from io.thecodeforge.main import app # assuming your FastAPI app is here
client = TestClient(app)
classTestCORS:
deftest_cors_headers_present(self):
response = client.get("/", headers={"Origin": "https://dashboard.thecodeforge.io"})
assert response.headers.get("access-control-allow-origin") == "https://dashboard.thecodeforge.io"assert response.headers.get("access-control-allow-credentials") == "true"deftest_cors_rejects_wrong_origin(self):
response = client.get("/", headers={"Origin": "https://evil.com"})
# No CORS headers means browser blocks itassert"access-control-allow-origin"notin response.headers
classTestShortCircuit:
deftest_blocked_ip_returns_403(self):
# Simulate an IP - this might not work in TestClient because request.client is None# We need to mock request.clientwithpatch("fastapi.Request.client", new_callable=PropertyMock) as mock_client:
mock_client.return_value = Mock(host="10.0.0.99")
response = client.get("/")
assert response.status_code == 403assert response.json() == {"detail": "Forbidden"}
💡Mocking request.client for IP Tests
The TestClient does not set request.client. If your middleware uses request.client.host, you need to mock it. Use unittest.mock.patch to mock fastapi.Request.client as a property. Alternatively, design your middleware to read from a trusted header like X-Forwarded-For first, which is easier to set in tests.
📊 Production Insight
Integration tests with TestClient catch most middleware issues, but not all. Production differences (middleware order when multiple services, ASGI server version) can cause subtle failures.
Solution: also run smoke tests against a real containerized deployment.
Rule: test middleware in isolation and in the full stack.
🎯 Key Takeaway
Middleware must be tested, especially short-circuit and header logic.
Mock request.client carefully.
Use both TestClient and live e2e tests.
CORS Preflight Requests: The OPTIONS Dance Your Browser Never Told You About
Your browser doesn't just blindly send cross-origin POST requests with custom headers. It first checks with the server using an HTTP OPTIONS request called a 'preflight.' If the server doesn't respond with the right CORS headers, the browser aborts the actual request before it even leaves the network stack.
This is where most CORS bugs live. You configure CORSMiddleware for GET and POST, but your frontend sends an Authorization header. The browser sends a preflight to check if that header's allowed. If your middleware doesn't include allow_headers=['Authorization'], the preflight fails silently and your JavaScript gets a cryptic CORS error.
The preflight response must include Access-Control-Allow-Origin, Access-Control-Allow-Methods, and Access-Control-Allow-Headers. FastAPI's CORSMiddleware handles this automatically when you configure it correctly — but only if you know the dance exists. Skip a header, and you're debugging for hours.
* Preflight fails client-side: 'Authorization' header not allowed
* Actual POST never sent. Browser blocks it.
⚠ Production Trap:
Browsers cache preflight responses. If you fix CORS headers after a failed preflight, the old response may still be cached for up to 5 minutes (Access-Control-Max-Age). Clear your cache or add a cache-busting query param during development.
🎯 Key Takeaway
Never trust your browser. Always test preflight with curl -X OPTIONS before assuming CORS works.
Allowing Any Origin With Credentials: The False Economy of the Wildcard
Setting allow_origins=["*"] sounds like a quick fix. It's not. The CORS spec explicitly forbids using a wildcard when allow_credentials=True. Why? Because a wildcard tells the browser to accept any origin, but credentials (cookies, Authorization headers) must only be sent to known origins. Combining them is a security hole the spec refuses to open.
When you try to set both, FastAPI's CORSMiddleware throws a ValueError at startup. Not at runtime — at startup. You'll see: 'CORS middleware configuration: allow_origins must not be a wildcard when allow_credentials is True.'
The fix is explicit. List every origin you trust. If you have multiple environments (staging, production, local dev), maintain a list. Use environment variables. Never ship with credentials + wildcard. The JavaScript console error you'll get on the frontend is useless: 'Access to fetch at ... has been blocked by CORS policy.' No mention of the credentials conflict.
If you truly need dynamic origins (e.g., a multi-tenant SaaS), implement a custom origin validation function. FastAPI's CORSMiddleware accepts a callable for allow_origins. Validate the request's Origin header against a database or config. That's production-grade.
CredentialsWildcard.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// io.thecodeforge — python tutorial
from fastapi importFastAPIfrom fastapi.middleware.cors importCORSMiddleware
app = FastAPI()
# This will crash on startup:
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Correct: explicit originsfrom os import environ
origins = environ.get(
"ALLOWED_ORIGINS",
"https://app.example.com,http://localhost:3000"
).split(",")
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
)
@app.get("/api/user")
asyncdefget_user():
return {"user": "admin"}
Output
$ uvicorn main:app
ValueError: CORS middleware configuration: allow_origins must not be a wildcard when allow_credentials is True.
# After fix:
$ uvicorn main:app
INFO: Application startup complete.
🔥Senior Shortcut:
Use a domain-based allow list with environment variables. Never hardcode origins. For local development, use 'http://localhost:3000' explicitly — never a wildcard.
🎯 Key Takeaway
Wildcard + credentials = startup crash. Always maintain an explicit origin list for production CORS configs.
CORS Middleware Is Not a Security Wall — It's a Browser Policy Enforcer
Juniors treat CORS middleware like a firewall. It's not. The browser enforces CORS, not your server. If you curl your API from a terminal, CORS headers are ignored. No browser, no CORS.
FastAPI's CORSMiddleware appends Access-Control-Allow-Origin to responses. That tells the browser: "This origin is allowed to read the response." Without it, the browser blocks JavaScript from reading cross-origin data—even if the server sends it.
This is critical for SPAs, mobile backends, and third-party API consumers. But understand the weakness: any non-browser client (curl, Postman, server-to-server) bypasses CORS entirely. So don't rely on CORS for auth. It's a UX enabler, not an access control gate. Use real auth middleware for security.
In production, never expose origins you don't explicitly trust. That includes * with credentials—we killed that myth in another section.
cors_basics.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// io.thecodeforge — python tutorial
from fastapi importFastAPIfrom fastapi.middleware.cors importCORSMiddleware
app = FastAPI()
origins = [
"https://myapp.com",
"https://admin.myapp.com",
]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/data")
asyncdefget_data():
return {"secret": "only browsers care"}
# curl http://localhost:8000/data# -> {"secret": "only browsers care"} # No CORS check – works fine
Never use allow_origins=["*"] with allow_credentials=True. The spec forbids it—browsers will reject the wildcard. You'll get mysterious CORS errors in production. List explicit origins.
🎯 Key Takeaway
CORS only blocks browsers. Your API security lives in authentication middleware, not CORS headers.
The CORSMiddleware Constructor: Tuning Origin, Method, and Header Whitelists
Eight parameters. Each one a footgun if you set it wrong. Let's skip the obvious ones and hit the killers.
allow_origins: A list of exact origins, or [""] for any. But [""] breaks with credentials. Prod tip: use allow_origin_regex for subdomain patterns like https://.*\.myapp\.com. Keeps the list tight.
allow_methods: Defaults to ["GET"]. If your frontend sends POST or DELETE, add them explicitly or use ["*"] for simplicity. But lazy wildcards leak attack surface—only allow methods your API actually exposes.
allow_headers: Same deal. If you send custom headers like X-Request-ID, list them. Using ["*"] is fine for open APIs, but for enterprise, whitelist.
expose_headers: The forgotten one. By default, browsers only expose a handful of response headers to JS. If your frontend needs X-Total-Count or Content-Disposition, add them here or JS gets null.
Set them once, test with curl and a browser, and don't touch again.
Test CORS with Chrome devtools Network tab. Look for the access-control-allow-origin header in the preflight (OPTIONS) response. Missing it? Your frontend dies silently.
🎯 Key Takeaway
Whitelist origins, methods, and headers explicitly. Expose custom headers your frontend needs. Prod is not a playground—no * with credentials.
● Production incidentPOST-MORTEMseverity: high
The Middleware That Silently Dropped All Requests
Symptom
After a routine middleware update, every endpoint returned 504 Gateway Timeout. No errors in the application logs, but the middleware logs showed requests entering but never leaving.
Assumption
The new middleware was non-blocking and would naturally pass through to the route handler. The team assumed call_next was being awaited correctly.
Root cause
The middleware function was modifying the response object before calling call_next and then returning a cached response object without awaiting the actual response. call_next was invoked but its result was never assigned—the middleware returned a placeholder response early.
Fix
Restructured the middleware to always await call_next(request) first, then modify the returned response. Added explicit error logging to verify that call_next completes. Deployed additional integration tests that verify middleware pass-through.
Key lesson
Never modify the response before calling call_next unless you intend to short-circuit the request.
Always log a trace message after call_next returns to confirm execution.
Add a health endpoint that bypasses all custom middleware to detect this class of failures.
Use mypy or pyright to catch missing await on async functions at development time.
Production debug guideSymptom → Action for Production Issues5 entries
Symptom · 01
Endpoint returns empty response (no body)
→
Fix
Check if a middleware modified the response body after call_next. Verify that middleware doesn't accidentally consume the body stream without re-wrapping it.
Symptom · 02
All routes return 503 Service Unavailable
→
Fix
Verify call_next is awaited in every middleware. Use timeout middleware to detect blocking middleware.
Symptom · 03
CORS errors only in production (works locally)
→
Fix
Check allow_origins list. Ensure wildcard * is not paired with allow_credentials=True. Verify that CORS headers are present on error responses.
Symptom · 04
Request hangs indefinitely
→
Fix
Add a timeout to the outermost middleware. Use asyncio.wait_for to fail fast if any middleware blocks.
Symptom · 05
Custom header not reaching the route handler
→
Fix
Confirm the header is set before call_next on request.state or request.headers. Be aware that request.headers is immutable in some contexts.
★ FastAPI Middleware Quick Debug Cheat SheetCommands and immediate actions for common middleware problems in production.
API responds with 504 Gateway Timeout after middleware change−
Immediate action
Pause the middleware change. Restore the previous version. Then reproduce locally.
Move trace ID injection to a middleware that runs first (added last) and log it immediately after set. Use a middleware-specific logger.
Middleware throws AttributeError: 'Request' object has no attribute 'state'+
Immediate action
Do not rely on `request.state` in ASGI middleware level. Use `request.scope` instead for middleware-scoped data.
Commands
python -c "from fastapi import Request; r = Request(scope={'type':'http'}); print(hasattr(r,'state'))"
grep 'request.state' app/middleware/*.py -n
Fix now
Set request.state.trace_id in the middleware that runs first, before passing to others. Ensure all middlewares that read it run after.
Middleware vs Dependency for Cross-Cutting Concerns
Concern
Middleware
Dependency (Depends())
Execution timing
Before route matching
After route matching
Access to route parameters
No
Yes
Can short-circuit request
Yes (return Response)
No (raises exception)
Modify response headers
Yes
Limited (via response) but harder
Performance overhead per request
Always runs for all routes
Only runs when used in route
Example use cases
CORS, logging, IP blocking, compression
Auth, pagination, DB sessions
⚙ Quick Reference
10 commands from this guide
File
Command / Code
Purpose
iothecodeforgemiddlewaresecurity.py
from fastapi import FastAPI
CORS Middleware
iothecodeforgemiddlewarelogging.py
from fastapi import FastAPI, Request
Custom Middleware
iothecodeforgemiddlewareorder.py
from fastapi import FastAPI
Understanding Middleware Execution Order
iothecodeforgemiddlewareip_blocker.py
from fastapi import FastAPI, Request, Response
Short-Circuiting Middleware
iothecodeforgemiddlewareerror_handler.py
from fastapi import FastAPI, Request, Response
Error Handling Middleware
iothecodeforgeteststest_middleware.py
from fastapi.testclient import TestClient
Testing Middleware with FastAPI TestClient
PreflightDebug.py
from fastapi import FastAPI
CORS Preflight Requests
CredentialsWildcard.py
from fastapi import FastAPI
Allowing Any Origin With Credentials
cors_basics.py
from fastapi import FastAPI
CORS Middleware Is Not a Security Wall
cors_tuning.py
from fastapi import FastAPI
The CORSMiddleware Constructor
Key takeaways
1
Middleware added last runs first for requests, and last for responses (LIFO order).
2
Global Context
Middleware is protocol-agnostic regarding specific routes; it sees all traffic including 404s and health checks.
3
The 'No Wildcard' Rule
You cannot use allow_origins=['*'] if allow_credentials is set to True due to W3C security specs.
4
State Sharing
Use request.state to pass variables (like user IDs or trace IDs) from middleware into your endpoint logic.
5
Avoid Blocking
Never perform heavy synchronous I/O inside a middleware's async def without utilizing threads, as it will block the entire event loop for all users.
6
Short-circuiting is for early exits, but ensure mandatory middlewares run first or you'll miss CORS headers and logs.
7
Test middleware with both TestClient and live e2e tests
differences exist between them.
Common mistakes to avoid
4 patterns
×
Using allow_origins=['*'] with allow_credentials=True
Symptom
Browser shows CORS error for requests that include cookies or Authorization headers. The preflight OPTIONS request returns 200 but missing Access-Control-Allow-Origin header, or returns * and browser rejects it.
Fix
Set explicit origins like ['https://example.com']. For dynamic origins, implement a middleware that checks the Origin header against a whitelist and sets it per request.
×
Forgetting to `await call_next(request)` in an async middleware
Symptom
The middleware appears to work, but requests hang indefinitely or return with incorrect response body (often empty or incomplete). No errors are raised because the coroutine is never awaited.
Fix
Always assign await call_next(request) to a variable. Use a linter rule (like flake8-async) to detect missing await on async functions. Add a timeout to the outermost middleware to fail fast.
Under load, the API becomes unresponsive. Only one request at a time can pass through the middleware. Latency increases linearly with concurrent requests.
Fix
Move blocking operations (e.g., time.sleep(), requests.get(), file I/O) into a background thread using asyncio.to_thread() or loop.run_in_executor(). Or use non-blocking alternatives (e.g., httpx.AsyncClient).
×
Modifying `request.state` in one middleware but reading it in another without ordering guarantee
Symptom
AttributeError: 'Request' object has no attribute 'state' in some routes, or the state is not present when expected. This happens when the middleware that sets the state runs after the one that reads it.
Fix
Define a clear middleware order: set state in the outermost middleware (added last) so it runs first on the request. Then all inner middleware and route handlers can access it. Document the order in a comment.
INTERVIEW PREP · PRACTICE MODE
Interview Questions on This Topic
Q01SENIOR
Describe the execution flow of multiple middlewares in FastAPI. If Middl...
Q02SENIOR
Why is it a security risk to allow all origins (`'*'`) in a production A...
Q03SENIOR
How does the ASGI 'scope' differ from the FastAPI 'Request' object, and ...
Q04SENIOR
Explain the 'Short-Circuiting' behavior: How can a middleware return a r...
Q05SENIOR
Scenario: You need to implement IP-based rate limiting. Would you do thi...
Q01 of 05SENIOR
Describe the execution flow of multiple middlewares in FastAPI. If Middleware A is added before Middleware B, which one sees the Response object first?
ANSWER
In FastAPI, middleware execution follows a LIFO (Last-In-First-Out) order for the request phase and FIFO for the response phase. Middleware A added before B means B is the outermost layer (because it was added last). So the request hits B first, then A, then the route handler. On the response, the order is reversed: the response passes through A first, then B. Therefore, B sees the response first (before A). This is the onion model. Code example:
``python
app.add_middleware(A)
app.add_middleware(B)
# Request: B -> A -> route
# Response: route -> A -> B
``
So B sees the response first.
Q02 of 05SENIOR
Why is it a security risk to allow all origins (`'*'`) in a production API that uses JWT cookies for authentication?
ANSWER
JWT cookies are credentials. The CORS specification requires that when allow_credentials=True, the Access-Control-Allow-Origin header must be an explicit origin (not ). If you set allow_origins=[''] with allow_credentials=True, browsers will reject the preflight response and the request will fail. More importantly, even if it were technically allowed, allowing all origins means any website can make a credentialed request to your API. If the browser has your cookie, the malicious site could read the response. That's why the spec forbids * with credentials. Always whitelist explicit trusted origins.
Q03 of 05SENIOR
How does the ASGI 'scope' differ from the FastAPI 'Request' object, and how would you access it inside a low-level middleware?
ANSWER
The ASGI scope is a dictionary passed by the server for each connection. It contains raw ASGI protocol data such as type, http_version, headers (list of bytes tuples), method, path, query_string, client (host, port), server, etc. The FastAPI Request object is a convenience wrapper over the scope that provides parsed attributes like request.headers (a dict), request.url, request.method, etc. In a low-level ASGI middleware (not using @middleware decorator but implementing the ASGI interface directly), you receive the scope directly. You can access it as scope['client'] etc. In a FastAPI middleware, you can access request.scope to get the raw dictionary. For example, to read a raw header by bytes: request.scope['headers']`.
Q04 of 05SENIOR
Explain the 'Short-Circuiting' behavior: How can a middleware return a response without ever calling the `call_next` function?
ANSWER
Short-circuiting means a middleware decides to return a response early, bypassing the rest of the middleware chain and the route handler. In FastAPI, you do this by not calling await call_next(request) and instead returning a Response object directly (e.g., return JSONResponse(status_code=403, content={"detail": "Forbidden"})). This is useful for IP blocking, rate limiting, or early authentication checks. However, it means that middlewares that would have run after this one (inner layers) will not execute. This can break CORS headers, logging, or trace ID injection if those middlewares are placed as inner layers. To ensure mandatory middlewares still run, place them as outer layers (added first). Also, if you short-circuit, you must handle the response headers yourself (e.g., add CORS headers manually).
Q05 of 05SENIOR
Scenario: You need to implement IP-based rate limiting. Would you do this in a FastAPI middleware or a Dependency? Justify your choice based on performance and 'Route Matching' logic.
ANSWER
IP-based rate limiting should be implemented in a middleware, not a dependency. Reasons:
1. Performance: The rate limiter must check every request, including those that may never reach a route (e.g., health checks, static files). Middleware runs before route matching, so it can block requests early with minimal overhead. A dependency runs only after the route is matched, incurring additional overhead for matched routes.
2. Route independence: Rate limiting is a cross-cutting concern that should apply to all routes uniformly. Middleware naturally applies to all HTTP methods and endpoints. Dependencies require explicit inclusion in each route, increasing risk of missing some.
3. Early rejection: If a request is rate-limited, you want to reject it as early as possible to free up server resources. Middleware does this before any route logic executes.
4. Storage: Middleware has access to request.client.host before any dependency injection, making it straightforward to extract IP. However, if you need to inspect route parameters (e.g., rate-limit per user ID from JWT), you might consider a dependency that runs with the route context. But for IP-based, middleware is the right choice.
01
Describe the execution flow of multiple middlewares in FastAPI. If Middleware A is added before Middleware B, which one sees the Response object first?
SENIOR
02
Why is it a security risk to allow all origins (`'*'`) in a production API that uses JWT cookies for authentication?
SENIOR
03
How does the ASGI 'scope' differ from the FastAPI 'Request' object, and how would you access it inside a low-level middleware?
SENIOR
04
Explain the 'Short-Circuiting' behavior: How can a middleware return a response without ever calling the `call_next` function?
SENIOR
05
Scenario: You need to implement IP-based rate limiting. Would you do this in a FastAPI middleware or a Dependency? Justify your choice based on performance and 'Route Matching' logic.
SENIOR
FAQ · 5 QUESTIONS
Frequently Asked Questions
01
What is the difference between middleware and a Depends() dependency?
Execution timing and scope are the key differences. Middleware executes at the 'Gateway' level before FastAPI even figures out which route should handle the request. This makes it perfect for logging and CORS. Dependencies (Depends()) execute after the route is matched but before the business logic runs. Use dependencies for logic that requires access to route parameters or endpoint-specific data.
Was this helpful?
02
Why am I getting CORS errors even after adding CORSMiddleware?
This usually happens due to one of three reasons: 1) allow_credentials=True with wildcard * for origins. 2) Your frontend is sending a custom header (e.g., X-Requested-With) that isn't included in your allow_headers list. 3) Middleware order: If you have another middleware that returns a response (like an Auth check) before the CORSMiddleware, the CORS headers won't be attached to the error response.
Was this helpful?
03
Is there a limit to how many middlewares I can add?
While there is no hard limit, each middleware layer adds a small amount of overhead to the request/response cycle. If you have 20+ middlewares, you may see an increase in latency. For complex transformations, consider moving logic into a background task or an external API gateway like Nginx or Kong.
Was this helpful?
04
Can I use FastAPI middleware with WebSocket endpoints?
Yes, but only with @app.middleware('http'). For WebSockets, you need to use a lower-level ASGI middleware that handles both HTTP and WebSocket scopes. FastAPI's built-in middleware only applies to HTTP connections. For WebSocket authentication or logging, implement a custom ASGI middleware that inspects the scope['type'] and applies logic accordingly.
Was this helpful?
05
How do I pass data from middleware to route handlers safely?
Use request.state to attach arbitrary data (e.g., request.state.user after authentication). This is a per-request dictionary that is cleared after the response. However, be careful: if you mutate request.state in a middleware that short-circuits, downstream middleware won't see it. Also, ensure you set state before the route handler runs—so in the outermost middleware (added last) during the request phase.
Every FastAPI concept with runnable in-browser examples — params, Pydantic, dependency injection, JWT auth, async, SQLAlchemy, testing, WebSockets, and Docker deployment. The interactive reference for production engineers.
N
NarenFounder & Principal Engineer
20+ years shipping production Python across data and backend systems. Everything here is grounded in real deployments.