Skip to main content

FastAPI Request Context

When building APIs with FastAPI, understanding how to manage and access data throughout the request lifecycle is essential. In this guide, we'll explore the concept of "request context" - how information can be stored, shared, and accessed during an API request.

What is Request Context?

Request context refers to the information and data that's associated with a specific HTTP request. This data persists throughout the entire lifecycle of the request - from when it first arrives at your server until the response is sent back to the client.

FastAPI provides several mechanisms to store and access this contextual information:

  1. Request state - attaching data directly to the request object
  2. Dependency injection - sharing dependencies across request handlers
  3. Request-scoped variables - variables that exist only within a single request

Let's explore each of these approaches in detail.

Request State

FastAPI allows you to attach custom data to the request object using the request.state attribute.

Basic Usage of Request State

python
from fastapi import FastAPI, Request

app = FastAPI()

@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
# Store data in request.state
request.state.user_agent = request.headers.get("User-Agent")
request.state.client_host = request.client.host

# Process the request
response = await call_next(request)
return response

@app.get("/")
async def read_root(request: Request):
# Access data from request.state
return {
"user_agent": request.state.user_agent,
"client_host": request.state.client_host
}

In this example, we use a middleware to store the user agent and client IP address in the request state, making it accessible in any route handler that receives this request.

Dependency Injection for Context

FastAPI's dependency injection system is another powerful way to manage request context. Dependencies can be declared once and reused across multiple endpoints.

Creating a Context Dependency

python
from fastapi import FastAPI, Depends, Request
from typing import Optional

app = FastAPI()

async def get_user_context(request: Request):
"""Extract and return user context information from request."""
user_agent = request.headers.get("User-Agent", "")
auth_token = request.headers.get("Authorization", "").replace("Bearer ", "")

# You could validate the token, retrieve user info, etc.
# For this example, we'll just return some basic context
return {
"user_agent": user_agent,
"token": auth_token,
"ip_address": request.client.host
}

@app.get("/profile")
async def user_profile(context: dict = Depends(get_user_context)):
return {
"message": "Profile endpoint",
"client_info": context
}

@app.get("/settings")
async def user_settings(context: dict = Depends(get_user_context)):
return {
"message": "Settings endpoint",
"client_info": context
}

Both endpoints can now access the user context without duplicating code.

Request-Scoped Variables with Depends

FastAPI's Depends can create "request-scoped singletons" - objects that are created once per request and shared across all dependencies.

Creating Request-Scoped Variables

python
from fastapi import FastAPI, Depends, Request
import uuid
from datetime import datetime

app = FastAPI()

# This will create a new UUID for each request
async def get_request_id():
return str(uuid.uuid4())

# This dependency uses the request_id dependency
async def get_request_metadata(
request_id: str = Depends(get_request_id),
request: Request = None
):
return {
"request_id": request_id,
"timestamp": datetime.now().isoformat(),
"path": request.url.path if request else None
}

@app.get("/items")
async def read_items(metadata: dict = Depends(get_request_metadata)):
# Process the request
return {
"items": ["Item1", "Item2"],
"metadata": metadata
}

@app.get("/items/{item_id}")
async def read_item(
item_id: str,
metadata: dict = Depends(get_request_metadata)
):
return {
"item_id": item_id,
"metadata": metadata
}

Output from /items endpoint:

json
{
"items": ["Item1", "Item2"],
"metadata": {
"request_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"timestamp": "2023-08-10T14:30:45.123456",
"path": "/items"
}
}

In this example, get_request_id generates a new UUID for each request, and this same ID is used in all dependencies within that request.

Practical Example: Authentication Context

Here's a more practical example using request context for authentication:

python
from fastapi import FastAPI, Depends, HTTPException, Request
from fastapi.security import OAuth2PasswordBearer
from typing import Optional

app = FastAPI()
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

# Fake database of users
fake_users_db = {
"johndoe": {
"username": "johndoe",
"full_name": "John Doe",
"email": "[email protected]",
"role": "admin"
}
}

async def get_current_user(token: str = Depends(oauth2_scheme)):
# In a real app, you would decode and verify the token
# For this example, we're using the token as the username
user = fake_users_db.get(token)
if not user:
raise HTTPException(
status_code=401,
detail="Invalid authentication credentials",
headers={"WWW-Authenticate": "Bearer"},
)
return user

class UserContext:
def __init__(self,
user: dict = Depends(get_current_user),
request: Request = None):
self.user = user
self.request_path = request.url.path if request else None
self.is_admin = user.get("role") == "admin"
self.timestamp = datetime.now().isoformat()

@app.get("/users/me")
async def read_own_info(context: UserContext = Depends(UserContext)):
return {
"user_info": context.user,
"request_info": {
"path": context.request_path,
"timestamp": context.timestamp,
"is_admin_access": context.is_admin
}
}

@app.get("/admin/users")
async def list_all_users(context: UserContext = Depends(UserContext)):
# Check if user has admin privileges
if not context.is_admin:
raise HTTPException(status_code=403, detail="Unauthorized access")

# Return all users (in a real app, would fetch from database)
return {
"users": list(fake_users_db.values()),
"accessed_by": context.user["username"],
"timestamp": context.timestamp
}

In this example, UserContext combines:

  • User authentication data
  • Request metadata
  • Authorization logic (admin check)

This context is then available to route handlers, making it easy to implement consistent authentication and authorization.

Advanced: Starlette Context

For more advanced cases, you might want to use Starlette's context (FastAPI is built on Starlette). This is useful for storing data that needs to be accessible from any part of your code during a request's lifecycle.

First, install the starlette-context package:

bash
pip install starlette-context

Then use it in your FastAPI application:

python
from fastapi import FastAPI, Request
from starlette.middleware import Middleware
from starlette.middleware.base import BaseHTTPMiddleware
from starlette_context import context
from starlette_context.middleware import RawContextMiddleware

app = FastAPI(middleware=[
Middleware(RawContextMiddleware)
])

class LoggingMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
# Store data in context
context["request_id"] = str(uuid.uuid4())
context["start_time"] = time.time()

response = await call_next(request)

# You can access the context data here too
processing_time = time.time() - context["start_time"]
print(f"Request {context['request_id']} took {processing_time:.4f} seconds")

return response

app.add_middleware(LoggingMiddleware)

@app.get("/items")
async def read_items():
# Access context data from anywhere in the request lifecycle
request_id = context.get("request_id", "unknown")

# Your business logic here
items = ["Item1", "Item2"]

return {
"items": items,
"request_id": request_id
}

Summary

FastAPI provides several mechanisms for managing request context:

  1. Request State - Simple way to store data directly on the request object
  2. Dependency Injection - More organized approach for sharing data and logic
  3. Request-Scoped Variables - Variables shared within a single request
  4. Starlette Context - Advanced context management for complex applications

Understanding these approaches allows you to:

  • Keep your code DRY (Don't Repeat Yourself)
  • Maintain consistent behavior across endpoints
  • Implement cross-cutting concerns like logging, authentication, and monitoring
  • Create more maintainable and testable APIs

Additional Resources

Exercises

  1. Create a middleware that tracks request processing time and adds it to the request state.
  2. Implement a dependency that provides database connection information for each request.
  3. Build a context manager for user authorization that checks permissions for different API endpoints.
  4. Create a logging system that includes request context information like request IDs and user information.


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