Skip to main content

FastAPI Response Modification Middleware

In modern web application development, middleware plays a crucial role in processing requests and responses. FastAPI, a high-performance Python web framework, provides robust middleware capabilities that allow you to inspect and modify both incoming requests and outgoing responses. In this tutorial, we'll focus specifically on how to create middleware that modifies responses before they're sent back to the client.

Understanding Middleware in FastAPI

Before diving into response modification, let's understand what middleware is:

Middleware is code that runs before the request is processed by your route handlers or after the response is generated but before it's sent back to the client. In FastAPI, middleware is executed in the order it's added to your application.

A middleware component can:

  • Process request before it reaches your endpoint functions
  • Process response after your endpoint function returns but before it's sent to the client
  • Add headers, cookies, or modify the content of responses
  • Handle errors, logging, or authentication

Why Modify Responses with Middleware?

There are many reasons you might want to modify responses:

  • Adding standard security headers to all responses
  • Compressing response data
  • Adding cache control headers
  • Monitoring response times
  • Modifying JSON response structure consistently
  • Injecting CORS headers
  • Adding custom headers for tracking or diagnostics

Creating a Response Modification Middleware

Let's start with a basic example of creating middleware that modifies responses in a FastAPI application:

python
from fastapi import FastAPI, Request
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import Response
from typing import Callable

app = FastAPI()

class CustomHeaderMiddleware(BaseHTTPMiddleware):
async def dispatch(
self, request: Request, call_next: Callable
) -> Response:
# Process the request (if needed)

# Get the response from the route handler
response = await call_next(request)

# Modify the response
response.headers["X-Custom-Header"] = "CustomValue"

return response

# Add the middleware to the application
app.add_middleware(CustomHeaderMiddleware)

@app.get("/")
async def root():
return {"message": "Hello World"}

In this example:

  1. We created a custom middleware class that inherits from BaseHTTPMiddleware
  2. We implemented the dispatch method which:
    • Takes a request and a call_next function
    • Calls the call_next function to get the response from the route handler
    • Adds a custom header to the response
    • Returns the modified response
  3. We added our middleware to the FastAPI application

When a client makes a request to the root endpoint, they'll get a JSON response with a message, and also the custom header X-Custom-Header: CustomValue.

Modifying Response Content

To modify the actual content of responses, you'll need to work with the specific response type. Here's an example that modifies JSON responses:

python
import json
from fastapi import FastAPI, Request
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import Response, JSONResponse
from typing import Callable

app = FastAPI()

class ResponseModifierMiddleware(BaseHTTPMiddleware):
async def dispatch(
self, request: Request, call_next: Callable
) -> Response:
response = await call_next(request)

# Check if the response is a JSONResponse
if isinstance(response, JSONResponse):
# Get the current response body
body = response.body

# Decode the JSON content
data = json.loads(body)

# Add additional information
data["api_version"] = "1.0"
data["processed_by"] = "middleware"

# Create a new response with modified content
new_content = json.dumps(data).encode("utf-8")
return Response(
content=new_content,
status_code=response.status_code,
headers=dict(response.headers),
media_type=response.media_type
)

return response

app.add_middleware(ResponseModifierMiddleware)

@app.get("/items")
async def get_items():
return {"items": ["apple", "banana", "orange"]}

If you make a GET request to /items, the response would look like:

json
{
"items": ["apple", "banana", "orange"],
"api_version": "1.0",
"processed_by": "middleware"
}

Performance Monitoring Middleware Example

Here's a practical example of a middleware that measures response time and adds it as a header:

python
import time
from fastapi import FastAPI, Request
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import Response

app = FastAPI()

class TimingMiddleware(BaseHTTPMiddleware):
async def dispatch(
self, request: Request, call_next: Callable
) -> Response:
# Record start time
start_time = time.time()

# Process the request and get the response
response = await call_next(request)

# Calculate processing time
process_time = time.time() - start_time

# Add processing time header (in milliseconds)
response.headers["X-Process-Time-MS"] = str(round(process_time * 1000, 2))

return response

app.add_middleware(TimingMiddleware)

@app.get("/fast")
async def fast_route():
return {"message": "This is a fast route"}

@app.get("/slow")
async def slow_route():
# Simulate a slow operation
time.sleep(1)
return {"message": "This is a slow route"}

When accessing these endpoints, the response will include an X-Process-Time-MS header showing how long the request took to process. The /fast route will show a small value while the /slow route will show approximately 1000ms.

Conditional Response Modification

Sometimes, you only want to modify certain responses. Here's how you can apply modifications selectively:

python
from fastapi import FastAPI, Request
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import Response, JSONResponse

app = FastAPI()

class ConditionalMiddleware(BaseHTTPMiddleware):
async def dispatch(
self, request: Request, call_next: Callable
) -> Response:
response = await call_next(request)

# Only modify responses from specific paths
if request.url.path.startswith("/api/"):
response.headers["X-API-Version"] = "v1"

# Add different headers for different HTTP methods
if request.method == "GET":
response.headers["Cache-Control"] = "max-age=60"
elif request.method == "POST":
response.headers["X-Post-Processed"] = "true"

return response

app.add_middleware(ConditionalMiddleware)

@app.get("/api/users")
async def get_users():
return {"users": ["Alice", "Bob"]}

@app.get("/public/info")
async def get_info():
return {"info": "This is public"}

In this example, only responses from paths starting with /api/ will get the X-API-Version header. Additionally, GET requests get a cache control header while POST requests get a different header.

Error Handling in Response Middleware

If you're modifying responses, it's important to handle errors properly:

python
from fastapi import FastAPI, Request, HTTPException
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import Response, JSONResponse

app = FastAPI()

class ErrorHandlingMiddleware(BaseHTTPMiddleware):
async def dispatch(
self, request: Request, call_next: Callable
) -> Response:
try:
response = await call_next(request)

# Normal response modification
if isinstance(response, JSONResponse):
response.headers["X-Success"] = "true"

return response

except Exception as e:
# Create a custom error response
content = {
"error": True,
"message": str(e),
"path": request.url.path
}
return JSONResponse(
status_code=500,
content=content,
headers={"X-Error-Handled-By": "middleware"}
)

app.add_middleware(ErrorHandlingMiddleware)

@app.get("/safe")
async def safe_route():
return {"status": "ok"}

@app.get("/error")
async def error_route():
# Simulate an error
raise ValueError("Something went wrong")

This middleware catches exceptions and returns a custom error response with additional debugging information.

Modifying Response Status Codes

You can also modify status codes when needed:

python
from fastapi import FastAPI, Request
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import Response, JSONResponse

app = FastAPI()

class StatusCodeMiddleware(BaseHTTPMiddleware):
async def dispatch(
self, request: Request, call_next: Callable
) -> Response:
response = await call_next(request)

# For demonstration, convert 404 errors to custom responses
if response.status_code == 404:
return JSONResponse(
content={"error": "Resource not found", "path": request.url.path},
status_code=404,
headers={"X-Custom-Not-Found": "true"}
)

return response

app.add_middleware(StatusCodeMiddleware)

With this middleware, all 404 errors will have a consistent JSON structure and a custom header.

Middleware Response Compression Example

Here's a more advanced example of middleware that compresses responses:

python
import gzip
from fastapi import FastAPI, Request
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import Response

app = FastAPI()

class GzipMiddleware(BaseHTTPMiddleware):
async def dispatch(
self, request: Request, call_next: Callable
) -> Response:
# Check if client accepts gzip encoding
accept_encoding = request.headers.get("Accept-Encoding", "")

response = await call_next(request)

# Only compress if client supports it and response is compressible
if ("gzip" in accept_encoding.lower() and
response.headers.get("Content-Type", "").startswith(("text/", "application/json"))):

# Get the original response content
body = response.body

# Compress the body
compressed_body = gzip.compress(body)

# Create new response headers
headers = dict(response.headers)
headers["Content-Encoding"] = "gzip"
headers["Content-Length"] = str(len(compressed_body))

# Return compressed response
return Response(
content=compressed_body,
status_code=response.status_code,
headers=headers,
media_type=response.media_type
)

return response

app.add_middleware(GzipMiddleware)

This middleware compresses responses with gzip when the client indicates support for it through the Accept-Encoding header.

Order of Multiple Middlewares

It's important to understand that middleware executes in the order it's added. The request flows through middleware in order, and the response flows back through middleware in reverse order:

python
from fastapi import FastAPI, Request
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import Response

app = FastAPI()

class FirstMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next: Callable) -> Response:
# This runs first for the request
print("First middleware - request phase")
response = await call_next(request)
# This runs last for the response
print("First middleware - response phase")
response.headers["X-First"] = "true"
return response

class SecondMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next: Callable) -> Response:
# This runs second for the request
print("Second middleware - request phase")
response = await call_next(request)
# This runs second-to-last for the response
print("Second middleware - response phase")
response.headers["X-Second"] = "true"
return response

# The order of adding middleware matters
app.add_middleware(FirstMiddleware)
app.add_middleware(SecondMiddleware)

@app.get("/")
async def root():
print("Route handler executing")
return {"message": "Hello World"}

The execution order would be:

  1. First middleware - request phase
  2. Second middleware - request phase
  3. Route handler executing
  4. Second middleware - response phase
  5. First middleware - response phase

The response will have both headers added, with X-Second added first, then X-First.

Summary

Response modification middleware in FastAPI provides a powerful way to transform, enhance, or standardize your API responses. Through middleware, you can:

  • Add standard headers to all responses
  • Modify response content consistently
  • Format error responses
  • Monitor performance metrics
  • Implement content compression
  • Apply business logic that should affect all responses

By placing this logic in middleware, you can keep your route handlers focused on their primary business logic, making your code more maintainable and following the Single Responsibility Principle.

Additional Resources and Exercises

Resources:

Exercises:

  1. Basic: Create a middleware that adds the current timestamp to all responses as a header.

  2. Intermediate: Implement a middleware that formats all error responses (4xx and 5xx status codes) to have a consistent JSON structure.

  3. Advanced: Develop a caching middleware that stores responses for GET requests and serves them from cache when the same request comes in again, with a configurable expiration time.

  4. Challenge: Create a middleware that conditionally modifies response content based on a query parameter, for example, adding more detailed information when ?verbose=true is present in the URL.

By practicing these exercises, you'll gain a deeper understanding of middleware in FastAPI and how to leverage it for response modification in your applications.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)