"""Request context middleware — captures client IP and User-Agent per request.""" # Import Awaitable, Callable from collections.abc from collections.abc import Awaitable, Callable # Import ContextVar from contextvars from contextvars import ContextVar # Import Request from fastapi from fastapi import Request # Import BaseHTTPMiddleware from starlette.middleware.base from starlette.middleware.base import BaseHTTPMiddleware # Import Response from starlette.responses from starlette.responses import Response # Assign request_ip = ContextVar("request_ip", default="") request_ip: ContextVar[str] = ContextVar("request_ip", default="") # Assign request_user_agent = ContextVar("request_user_agent", default="") request_user_agent: ContextVar[str] = ContextVar("request_user_agent", default="") # Define function resolve_client_ip def resolve_client_ip(request: Request) -> str: """Extract the real client IP, honouring ``X-Forwarded-For`` when present. Args: request (Request): The incoming Starlette/FastAPI request. Returns: str: The resolved client IP address, or ``"unknown"`` when unavailable. """ # Assign forwarded = request.headers.get("X-Forwarded-For") forwarded = request.headers.get("X-Forwarded-For") # Check: forwarded if forwarded: # Return forwarded.split(",")[0].strip() return forwarded.split(",")[0].strip() # Check: request.client if request.client: # Return request.client.host return request.client.host # Return "unknown" return "unknown" # Define class RequestContextMiddleware class RequestContextMiddleware(BaseHTTPMiddleware): """Middleware that captures client IP and User-Agent into context variables.""" # Define async function dispatch async def dispatch( self, # Entry: request request: Request, # Entry: call_next call_next: Callable[[Request], Awaitable[Response]], ) -> Response: """Store client IP and User-Agent in context vars for the current request. Args: request (Request): The incoming HTTP request. call_next (Callable[[Request], Awaitable[Response]]): The next middleware or route handler. Returns: Response: The HTTP response produced by the downstream handler. """ # Call request_ip.set() request_ip.set(resolve_client_ip(request)) # Call request_user_agent.set() request_user_agent.set(request.headers.get("User-Agent", "")) # Return await call_next(request) return await call_next(request)