Routing and Middleware
Middleware — Logging, CORS, Timing
Middleware in FastAPI acts as a wrapping layer that processes every incoming request before it reaches your route handlers and every outgoing response before it hits the client. It is essential for global concerns that apply across your entire application, such as tracking metrics, enforcing security headers, or sanitizing traffic. You should reach for middleware when your logic must execute regardless of the specific route endpoint being accessed or the specific HTTP verb being utilized.
Understanding the Middleware Lifecycle
Middleware is essentially a function that sits between the client and your path operations, intercepting the request-response lifecycle. When a request hits your server, the middleware intercepts it first, allowing you to manipulate headers, log details, or even terminate the request early. After the middleware performs its initial task, it calls the 'next' handler to pass control deeper into your application. Crucially, the response then flows back out through this same middleware, giving you a second opportunity to modify headers or perform post-processing tasks before the response is finally returned to the client. This 'wrapping' behavior is why middleware is so powerful; it provides a single, centralized location to implement cross-cutting concerns that would otherwise need to be redundantly duplicated inside every single individual endpoint function in your entire codebase.
from fastapi import FastAPI, Request
import time
app = FastAPI()
@app.middleware("http")
async def simple_middleware(request: Request, call_next):
# This part runs before the request is processed
print(f"Processing request: {request.url.path}")
# call_next passes the request to the next middleware or route
response = await call_next(request)
# This part runs after the response is generated
print("Finished processing request")
return responseImplementing Request Timing
Timing middleware is a classic use case for monitoring application performance. By capturing a high-resolution timestamp immediately upon entering the middleware and subtracting it from a second timestamp taken after the response is generated, you can accurately measure the total latency of each request. This is critical for identifying bottlenecks, such as slow database queries or inefficient business logic, without needing to manually add timing logic to every endpoint. Because the middleware waits for the 'await call_next(request)' to finish, the returned time represents the complete round-trip duration through your router, validation, and endpoint processing. This observability allows developers to implement automated performance alerting or include timing metadata in custom response headers, which is invaluable for debugging production issues where latency spikes might be intermittent or specific to certain payload sizes or request types.
import time
@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
start_time = time.perf_counter()
response = await call_next(request)
process_time = time.perf_counter() - start_time
# Add a custom header with the time taken to process the request
response.headers["X-Process-Time"] = str(process_time)
return responseLogging Requests and Responses
Logging middleware provides an audit trail of how your application interacts with the outside world. Instead of simply relying on access logs provided by the web server, custom middleware allows you to extract specific details—such as User-Agent strings, specific header values, or even internal error codes—that are relevant to your business context. By wrapping the call_next execution in a try-except block, you can ensure that even when an endpoint crashes with an unhandled exception, your middleware can log the failure before passing the error up. This level of granular insight ensures that you can trace requests through your system, which is vital in a distributed environment or a complex monolithic application where identifying the source of a problematic request is otherwise like looking for a needle in a haystack. Properly structured logs allow for later analysis of usage patterns, security audits, and debugging.
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("http_logger")
@app.middleware("http")
async def log_requests(request: Request, call_next):
logger.info(f"Endpoint hit: {request.method} {request.url.path}")
try:
response = await call_next(request)
return response
except Exception as e:
logger.error(f"Unhandled error: {e}")
raise eManaging Cross-Origin Resource Sharing (CORS)
CORS middleware is a mandatory security configuration when your application serves as an API for web-based frontends hosted on different domains. Browsers enforce the Same-Origin Policy, which prevents scripts on one domain from accessing data from another unless explicitly permitted by the server. Using the built-in CORSMiddleware is the standard way to inform browsers which origins, methods, and headers are acceptable. By setting 'allow_origins', you define a whitelist of trusted sources. Without this middleware, your API would fail to respond to cross-origin AJAX requests, effectively rendering it useless for standard web applications. It is important to realize that this middleware doesn't just block requests; it dynamically handles 'preflight' OPTIONS requests sent by the browser. By offloading these complex security negotiations to the middleware layer, you keep your route handlers focused solely on business logic, ensuring that your security posture is consistent and easily auditable across all exposed routes.
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000"], # List of allowed domains
allow_credentials=True,
allow_methods=["GET", "POST"], # Allowed HTTP verbs
allow_headers=["*"], # Allow all headers
)Best Practices for Middleware Complexity
While middleware is powerful, it is also a global singleton; poorly written middleware can introduce latency or bugs into every request. Keep your middleware logic as lean as possible; avoid performing expensive I/O operations or database lookups directly inside the middleware unless absolutely necessary, as this will force every single user request to bear that performance penalty. Always ensure that exceptions inside your middleware are handled gracefully so that you do not accidentally swallow errors that should be returned to the client as proper HTTP 500 responses. Furthermore, order matters significantly in FastAPI because middleware is executed in a 'last-added, first-executed' stack. If you have both logging and CORS middleware, the one added last will see the request first. By carefully sequencing your stack, you ensure that security checks occur before logging, providing a robust architecture that handles traffic predictably from the very first byte.
# Middleware stack order is important
# The last middleware added is the first one executed
app.add_middleware(CORSMiddleware, allow_origins=["*"])
# This logger will be called AFTER the CORS middleware
@app.middleware("http")
async def global_logger(request, call_next):
print("Logging request inside global scope")
return await call_next(request)Key points
- Middleware operates by wrapping the entire request-response lifecycle of your FastAPI application.
- The call_next function is responsible for passing the request forward through the middleware chain.
- Middleware allows for centralized cross-cutting concerns like logging, timing, and security headers.
- FastAPI executes middleware in the order they were added, following a stack-based structure.
- Timing middleware provides useful performance metrics by measuring the duration between request and response.
- CORS middleware is required to permit web browsers to interact with your API from external domains.
- Heavy logic inside middleware can negatively impact the performance of every request hitting your server.
- Always ensure your middleware handles exceptions correctly to avoid blocking valid traffic or masking errors.
Common mistakes
- Mistake: Adding CORS middleware after defining all routes. Why it's wrong: Middleware processes requests in reverse order; if CORS is added last, it may not intercept OPTIONS requests before they reach your route handlers. Fix: Always mount CORS middleware as one of the first lines after creating the FastAPI app instance.
- Mistake: Modifying the response object within a logging middleware. Why it's wrong: FastAPI responses are meant to be consumed by the client, and improperly iterating over the response body can consume the stream, leaving the client with an empty response. Fix: Use the `StreamingResponse` carefully or only log metadata like status codes and headers.
- Mistake: Assuming `process_time` header is automatically included. Why it's wrong: FastAPI does not track request duration automatically; you must manually calculate it using `time.time()` or `time.perf_counter()` inside the middleware. Fix: Calculate the time delta between start and end of the request and set it on the response headers.
- Mistake: Over-configuring CORS to allow everything (*). Why it's wrong: Using `allow_origins=['*']` with credentials enabled is a security risk that permits any site to perform authenticated requests on behalf of your users. Fix: Explicitly define the list of allowed origins and only use wildcard origins when credentials are not required.
- Mistake: Forgetting to call `await call_next(request)` in an asynchronous middleware. Why it's wrong: The request lifecycle will stall, resulting in the client receiving no response and the server hanging on that specific request. Fix: Ensure that `await call_next(request)` is always executed and returned.
Interview questions
What is middleware in FastAPI and what is its primary purpose?
Middleware in FastAPI is a function or class that executes on every request before it reaches a specific path operation, and on every response before it is sent to the client. Its primary purpose is to perform cross-cutting concerns that should apply globally, such as modifying request headers, managing security tokens, or tracking request lifecycles. By placing logic in middleware, you avoid duplicating code across multiple individual route handlers, ensuring a clean and consistent architectural flow for all incoming traffic to your application.
How do you enable CORS in a FastAPI application, and why is it necessary?
You enable Cross-Origin Resource Sharing (CORS) by using the CORSMiddleware included in the fastapi.middleware.cors module. You add it to your app instance using app.add_middleware(). It is necessary because browsers enforce a Same-Origin Policy for security, which prevents malicious scripts on one origin from reading data from another. By configuring this middleware with specific allowed origins, methods, and headers, you explicitly permit your backend to interact with frontend applications hosted on different domains, ensuring both security and proper client-server connectivity.
How can you implement custom logging for request processing time in FastAPI?
To log the processing time, you can create a custom middleware function decorated with @app.middleware('http'). Inside, you capture the current time using time.time() before calling await call_next(request). After the response is returned, you calculate the delta by subtracting the start time from the current time. This is useful because it provides immediate visibility into performance bottlenecks, allowing you to log the duration to your monitoring system so you can identify which endpoints are consistently slower than others during high load.
Compare using a custom @app.middleware('http') approach versus using Starlette's BaseHTTPMiddleware class.
Using the @app.middleware('http') decorator is the simpler, idiomatic FastAPI way for quick tasks like timing or headers. However, if you need more complex, reusable logic or state management, you should inherit from BaseHTTPMiddleware. BaseHTTPMiddleware allows you to encapsulate middleware logic into a class with its own state. The decorator approach is easier to read for simple tasks, but class-based middleware is significantly more robust and testable for complex production pipelines where maintainability and separation of concerns are critical.
How does the execution order of multiple middleware components work in FastAPI?
In FastAPI, middleware is executed in the reverse order of how it is added. If you call app.add_middleware() for LoggerMiddleware, then CORS, the order of execution for incoming requests will be the final middleware added first, followed by the first one added. This stack behavior mimics a 'last-in, first-out' pattern. Understanding this is vital because if a middleware at the beginning of the stack rejects a request, subsequent middleware layers will never be reached, which could result in expected headers or logs being omitted from the process.
What are the performance implications of adding middleware, and how should one optimize it?
Middleware runs on every single request, so any blocking I/O operation or expensive computation within the middleware function will exponentially increase the latency of your entire API. To optimize, ensure your middleware is strictly asynchronous by using 'async def'. Avoid performing database queries or external network calls inside middleware unless absolutely necessary, as these significantly block the event loop. Always cache static configurations, such as CORS origins, outside the request loop to prevent redundant object creation and keep the overhead per request as close to zero as possible.
Check yourself
1. When defining a custom middleware to calculate request duration, where should you place the call to `call_next(request)`?
- A.Before measuring the start time
- B.Immediately after the return statement
- C.Between measuring the start time and the end time
- D.Only inside a try-except block without returning
Show answer
C. Between measuring the start time and the end time
You must measure the time, await the next middleware/route handler, then measure the time again to get the delta. Option 0 measures nothing, Option 1 is unreachable code, and Option 3 fails to continue the request lifecycle.
2. What happens if you define a CORS middleware that specifies `allow_origins=['*']` and `allow_credentials=True`?
- A.FastAPI will accept it but issue a warning because it is insecure
- B.FastAPI will raise a validation error and fail to start the application
- C.The browser will automatically block all requests due to privacy settings
- D.The middleware will dynamically detect the origin and allow it
Show answer
B. FastAPI will raise a validation error and fail to start the application
FastAPI prevents this dangerous configuration at startup by raising an exception, as credentials cannot be sent with a wildcard origin. Options 0, 2, and 3 are incorrect because the framework proactively prevents this misconfiguration.
3. Why is it important to define CORS middleware before other custom middlewares in FastAPI?
- A.Because CORS headers must be added to the request object directly
- B.Because CORS handles the OPTIONS pre-flight request which must bypass logic in other middlewares
- C.Because FastAPI requires middleware to be sorted by execution speed
- D.Because CORS middleware overrides all other headers added by other middlewares
Show answer
B. Because CORS handles the OPTIONS pre-flight request which must bypass logic in other middlewares
CORS pre-flight (OPTIONS) requests need to be resolved quickly. If other middleware processes them first and fails, the CORS headers won't be sent. Options 0, 2, and 3 are incorrect regarding how middleware order impacts the request lifecycle.
4. If you want to log the response status code in a middleware, how do you access it?
- A.You read the response status directly from the request object
- B.You await the response object returned by `call_next` and then check its status_code attribute
- C.You check the app's global state after the request has finished
- D.You look at the headers of the incoming request
Show answer
B. You await the response object returned by `call_next` and then check its status_code attribute
The status code is a property of the response object returned after the application processing is finished. Option 0 is impossible, Option 2 is prone to race conditions, and Option 3 describes the request, not the response.
5. What is the primary function of the `call_next` parameter in a FastAPI middleware function?
- A.It stops the request chain and returns an immediate response
- B.It serves as a reference to the next middleware or the path operation function
- C.It allows you to bypass the authentication layer
- D.It triggers the garbage collector to clear the request memory
Show answer
B. It serves as a reference to the next middleware or the path operation function
`call_next` is the function that passes the request to the rest of the application stack. Option 0 describes the opposite of its purpose, Option 2 is incorrect because middleware order is independent of auth logic, and Option 3 is unrelated.