Skip to content
Home Python FastAPI Middleware — Logging, CORS and Custom Middleware

FastAPI Middleware — Logging, CORS and Custom Middleware

Where developers are forged. · Structured learning · Free forever.
📍 Part of: Python Libraries → Topic 45 of 51
Master FastAPI middleware: configure production-ready CORSMiddleware, implement custom request/response logging, and understand execution order for high-performance ASGI apps.
⚙️ Intermediate — basic Python knowledge assumed
In this tutorial, you'll learn
Master FastAPI middleware: configure production-ready CORSMiddleware, implement custom request/response logging, and understand execution order for high-performance ASGI apps.
  • Middleware added last runs first for requests, and last for responses (LIFO order).
  • Global Context: Middleware is protocol-agnostic regarding specific routes; it sees all traffic including 404s and health checks.
  • The 'No Wildcard' Rule: You cannot use allow_origins=['*'] if allow_credentials is set to True due to W3C security specs.
✦ Plain-English analogy ✦ Real code with output ✦ Interview questions
Quick Answer
  • 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.
🚨 START HERE
FastAPI Middleware Quick Debug Cheat Sheet
Commands and immediate actions for common middleware problems in production.
🟡API responds with 504 Gateway Timeout after middleware change
Immediate ActionPause the middleware change. Restore the previous version. Then reproduce locally.
Commands
curl -v http://localhost:8000/health --connect-timeout 5
docker logs $(docker ps -q -f name=fastapi) 2>&1 | grep -i 'call_next'
Fix NowAdd a health check endpoint that bypasses middleware: `@app.get('/health')` before any middleware definition.
🟡CORS preflight (OPTIONS) request fails
Immediate ActionCheck response headers. Look for `Access-Control-Allow-Origin`. Use a simple GET request to isolate.
Commands
curl -X OPTIONS -I https://api.example.com/data
docker compose logs | grep -i 'cors'
Fix NowEnsure `allow_origins` includes the exact origin (not '*') when `allow_credentials=True`. Add missing headers to `allow_headers`.
🟡Custom trace ID not appearing in application logs
Immediate ActionCheck if `request.state.trace_id` is set before calling `call_next`. Verify the downstream code reads `request.state.trace_id`.
Commands
curl -v http://localhost:8000/test | jq '.headers'
grep 'trace_id' app/logs/*.log
Fix NowMove 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 ActionDo 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 NowSet `request.state.trace_id` in the middleware that runs first, before passing to others. Ensure all middlewares that read it run after.
Production IncidentThe Middleware That Silently Dropped All RequestsA misconfigured logging middleware caused a full API outage. Requests were received but never forwarded to the application handler, resulting in 504 timeouts across all endpoints.
SymptomAfter 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.
AssumptionThe new middleware was non-blocking and would naturally pass through to the route handler. The team assumed call_next was being awaited correctly.
Root causeThe 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.
FixRestructured 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 Issues
Endpoint returns empty response (no body)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.
All routes return 503 Service UnavailableVerify call_next is awaited in every middleware. Use timeout middleware to detect blocking middleware.
CORS errors only in production (works locally)Check allow_origins list. Ensure wildcard * is not paired with allow_credentials=True. Verify that CORS headers are present on error responses.
Request hangs indefinitelyAdd a timeout to the outermost middleware. Use asyncio.wait_for to fail fast if any middleware blocks.
Custom header not reaching the route handlerConfirm the header is set before call_next on request.state or request.headers. Be aware that request.headers is immutable in some contexts.

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.

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.py · PYTHON
12345678910111213141516171819202122
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

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 ['*']
▶ Output
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://dashboard.thecodeforge.io
⚠ Wildcard + Credentials = Error
  • 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.
When to use which origin setting
IfSingle frontend domain, needs cookies/auth headers
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.py · PYTHON
12345678910111213141516171819202122232425262728293031
from fastapi import FastAPI, Request
import time
import uuid
import logging

app = FastAPI()
logger = logging.getLogger("thecodeforge.access")

@app.middleware('http')
async def add_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 = await call_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
▶ Output
→ RID: 550e8400-e29b-41d4-a716-446655440000 | Path: /api/v1/users | Time: 0.0042s
Mental Model
The Onion Execution Model
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.

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.

io/thecodeforge/middleware/order.py · PYTHON
12345678910111213141516171819202122232425262728293031
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()

# Execution order (request direction):
# 1. Security headers middleware (added last)
# 2. CORSMiddleware
# 3. Logging middleware (added first)
# 4. Route handler
# Response order is reverse

app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://example.com"],
    allow_credentials=True,
    allow_headers=["*"],
)

@app.middleware("http")
async def security_headers(request, call_next):
    response = await call_next(request)
    response.headers["X-Content-Type-Options"] = "nosniff"
    return response

@app.middleware("http")
async def logging_middleware(request, call_next):
    print(f"Request: {request.method} {request.url}")
    response = await call_next(request)
    print(f"Response status: {response.status_code}")
    return response
Mental Model
Stack vs Queue for Request/Response
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.py · PYTHON
123456789101112131415161718192021
from fastapi import FastAPI, Request, Response
import logging

app = FastAPI()
logger = logging.getLogger("thecodeforge.security")

BLOCKED_IPS = {"10.0.0.99", "192.168.1.200"}

@app.middleware("http")
async def ip_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}")
        return Response(
            content='{"detail": "Forbidden"}',
            status_code=403,
            media_type="application/json"
        )
    # If not blocked, continue
    response = await call_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.py · PYTHON
12345678910111213141516171819202122232425
from fastapi import FastAPI, Request, Response
from fastapi.responses import JSONResponse
import logging
import traceback

thecodeforge_logger = logging.getLogger("thecodeforge.error")

app = FastAPI()

@app.middleware("http")
async def catch_exceptions_middleware(request: Request, call_next):
    try:
        response = await call_next(request)
        return response
    except Exception as 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 client
        return JSONResponse(
            status_code=500,
            content={"detail": "Internal server error. Please try again later.",
                      "trace_id": request.state.trace_id if hasattr(request, 'state') else None}
        )
🔥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.py · PYTHON
1234567891011121314151617181920212223242526
import pytest
from fastapi.testclient import TestClient
from io.thecodeforge.main import app  # assuming your FastAPI app is here

client = TestClient(app)

class TestCORS:
    def test_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"

    def test_cors_rejects_wrong_origin(self):
        response = client.get("/", headers={"Origin": "https://evil.com"})
        # No CORS headers means browser blocks it
        assert "access-control-allow-origin" not in response.headers

class TestShortCircuit:
    def test_blocked_ip_returns_403(self):
        # Simulate an IP - this might not work in TestClient because request.client is None
        # We need to mock request.client
        with patch("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 == 403
            assert 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.
🗂 Middleware vs Dependency for Cross-Cutting Concerns
When to use each pattern
ConcernMiddlewareDependency (Depends())
Execution timingBefore route matchingAfter route matching
Access to route parametersNoYes
Can short-circuit requestYes (return Response)No (raises exception)
Modify response headersYesLimited (via response) but harder
Performance overhead per requestAlways runs for all routesOnly runs when used in route
Example use casesCORS, logging, IP blocking, compressionAuth, pagination, DB sessions

🎯 Key Takeaways

  • Middleware added last runs first for requests, and last for responses (LIFO order).
  • Global Context: Middleware is protocol-agnostic regarding specific routes; it sees all traffic including 404s and health checks.
  • The 'No Wildcard' Rule: You cannot use allow_origins=['*'] if allow_credentials is set to True due to W3C security specs.
  • State Sharing: Use request.state to pass variables (like user IDs or trace IDs) from middleware into your endpoint logic.
  • 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.
  • Short-circuiting is for early exits, but ensure mandatory middlewares run first or you'll miss CORS headers and logs.
  • Test middleware with both TestClient and live e2e tests: differences exist between them.

⚠ Common Mistakes to Avoid

    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.

    Performing synchronous blocking I/O inside `async def` middleware
    Symptom

    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 Questions on This Topic

  • QDescribe the execution flow of multiple middlewares in FastAPI. If Middleware A is added before Middleware B, which one sees the Response object first?SeniorReveal
    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.
  • QWhy is it a security risk to allow all origins ('*') in a production API that uses JWT cookies for authentication?SeniorReveal
    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.
  • QHow does the ASGI 'scope' differ from the FastAPI 'Request' object, and how would you access it inside a low-level middleware?Mid-levelReveal
    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']`.
  • QExplain the 'Short-Circuiting' behavior: How can a middleware return a response without ever calling the call_next function?Mid-levelReveal
    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).
  • QScenario: 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.SeniorReveal
    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.

Frequently Asked Questions

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.

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.

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.

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.

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.

🔥
Naren Founder & Author

Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.

← PreviousFastAPI File Uploads and Form DataNext →FastAPI Testing with pytest and TestClient
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged