starlette-context

repository·master·Indexed 20 days ago

https://github.com/tomwojcik/starlette-context

Middleware for Starlette (version 0.5.1) that provides a request-scoped data store using Python's ContextVar. It allows storing and accessing context data throughout the request-response cycle, making it ideal for logging request headers like x-request-id or x-correlation-id. Includes built-in plugins for RequestId and CorrelationId, and a request_cycle_context manager for unit testing.

Tokens
16.7K
Snippets
52
Records
61
Agent score
69%

What's inside starlette-context

  1. Overview of starlette-context

    master

    starlette-context is a middleware library for the Starlette framework designed to maintain and manage request-specific context data throughout the entire request-response lifecycle.

    Key capabilities include:

    • Global Access: Access request headers and metadata from any part of your application without passing objects through every function.
    • ID Propagation: Easily propagate identifiers such as x-request-id or x-correlation-id.
    • Log Enrichment: Automatically include request context information in your application logs.
    • Custom Data Storage: Store and retrieve custom data that is scoped to the current request's lifecycle.
  2. How the context object works

    master

    The context object is implemented using Python's ContextVar. This ensures the data store is:

    • Thread-safe and async-compatible: Safe to use in asynchronous Starlette or FastAPI applications.
    • Request-scoped: Data is isolated to the specific request-response cycle.
    • Globally accessible: Can be imported and used in any part of your application (services, utilities, etc.) without needing to pass the request object through every function call.

    It is conceptually similar to Flask's g object.

  3. Choose between ContextMiddleware and RawContextMiddleware

    master

    The middleware is responsible for creating and managing the request context. You must choose one of the two available implementations based on your application's needs:

    • ContextMiddleware: Built on Starlette's BaseHTTPMiddleware. It is simpler to use and suitable for most standard web applications.
    • RawContextMiddleware: Operates at a lower ASGI level. Use this if your application relies heavily on StreamingResponse or handles very large responses, as it avoids memory issues and provides better performance for streaming.

    Both middlewares support the same plugins configuration.

    from starlette.applications import Starlette
    from starlette.middleware import Middleware
    from starlette_context.middleware import ContextMiddleware
    from starlette_context import plugins
    
    middleware = [
        Middleware(
            ContextMiddleware,
            plugins=(
                plugins.RequestIdPlugin(),
                plugins.CorrelationIdPlugin()
            )
        )
    ]
    
    app = Starlette(middleware=middleware)
  4. How to safely use context with FastAPI Background Tasks

    master

    The Problem

    When using ContextMiddleware, background tasks execute after the middleware's context manager has already exited and reset the context. While Python's ContextVar inheritance (PEP 567) might make context appear available in background tasks, this is an implementation detail and is not guaranteed.

    To ensure reliability, you must capture a copy of the context data during the request and pass it explicitly as an argument to your background task function.

    Do not attempt to call context.get() directly inside a background task function.

    from fastapi import FastAPI, BackgroundTasks
    from starlette_context import context
    from starlette_context.middleware import ContextMiddleware
    
    app = FastAPI()
    app.add_middleware(ContextMiddleware)
    
    def process_item(item_id: str, context_data: dict):
        # ✅ RECOMMENDED: Use explicitly passed context data
        # This is reliable and guaranteed to work
        print(f"Processing item {item_id} with context: {context_data}")
        request_id = context_data.get("X-Request-ID")
    
    @app.post("/items/{item_id}")
    async def create_item(item_id: str, background_tasks: BackgroundTasks):
        # Capture context data during request
        context_data = context.data.copy()
    
        # Pass context data explicitly to the background task
        background_tasks.add_task(process_item, item_id, context_data)
    
        return {"message": "Item will be processed"}
  5. How starlette-context works

    master

    The context object provides a way to store and access data during a request-response cycle. To use it, you must ensure two conditions are met:

    1. You are currently within a request-response cycle.
    2. You have added either ContextMiddleware or RawContextMiddleware to your ASGI application middleware stack.

    Once configured, you can use the context object to store arbitrary data (e.g., context["key"] = "value") and retrieve it later via context.data.

    from starlette_context import context
    
    # Inside a route handler
    context["user_id"] = "12345"
    print(context.data)
  6. Understand middleware exception handling limitations

    master

    Due to Starlette's exception handling mechanics, if an unhandled exception occurs in your application, the middleware's enrich_response method will not run.

    Consequences:

    • The middleware cannot set response headers for 500 error responses.
    • The default_error_response configured in the middleware will not be used for unhandled exceptions.
    • If you use custom exception handlers for 500 errors, be aware that the context may not be available within those handlers.
  7. Basic usage of starlette-context

    master

    To use starlette-context, add ContextMiddleware to your Starlette middleware stack. You can pass plugins to the middleware to automatically populate the context with data like CorrelationIdPlugin or RequestIdPlugin. Once the middleware is active, you can access and modify the request-scoped context using the context object within your routes or other middleware.

    from starlette.applications import Starlette
    from starlette.middleware import Middleware
    from starlette_context import context, plugins
    from starlette_context.middleware import ContextMiddleware
    
    # ... setup routes ...
    
    async def index(request):
        # Store data in the context
        context["custom_value"] = "This will be visible in logs"
        return JSONResponse(context.data)
    
    app = Starlette(
        routes=[Route("/", index)],
        middleware=[
            Middleware(
                ContextMiddleware,
                plugins=(
                    plugins.CorrelationIdPlugin(),
                    plugins.RequestIdPlugin(),
                ),
            ),
        ],
    )
  8. Enrich logs with context data using structlog

    master

    You can use starlette-context to automatically inject request-specific data into your logs. When using a logging library like structlog, create a processor that checks if a context exists before attempting to merge its data.

    Important: Always use the context.exists() guard. If you attempt to access context.data outside of a request cycle (such as during application startup), it will raise a ContextDoesNotExistError.

    import structlog
    from starlette_context import context
    
    def add_context(logger, method_name, event_dict):
        """Structlog processor that merges context data into every log entry."""
        if context.exists():
            event_dict.update(context.data)
        return event_dict
    
    structlog.configure(
        processors=[
            add_context,
            structlog.dev.ConsoleRenderer(),
        ]
    )
    
    logger = structlog.get_logger()
    
    # Inside a route
    @app.route("/")
    async def index(request):
        logger.info("Processing request")
        return JSONResponse({"message": "Hello World"})
  9. Configure plugins in ContextMiddleware

    master

    To use plugins to extract data from requests into your context, pass them to the ContextMiddleware during application initialization via the plugins argument. If no plugins are provided, the middleware creates an empty context that you must populate manually.

    from starlette.applications import Starlette
    from starlette.middleware import Middleware
    from starlette_context import plugins
    from starlette_context.middleware import ContextMiddleware
    
    middleware = [
        Middleware(
            ContextMiddleware,
            plugins=(
                plugins.RequestIdPlugin(),
                plugins.CorrelationIdPlugin()
            )
        )
    ]
    
    app = Starlette(middleware=middleware)
  10. Safely access context in FastAPI Exception Handlers

    master

    When writing custom FastAPI exception handlers, you should safely check if the context exists before attempting to access it. Use context.exists() or wrap your access in a try/except block catching ContextDoesNotExistError to prevent errors when an exception occurs outside of a valid request cycle.

    from fastapi import FastAPI, Request, HTTPException
    from fastapi.responses import JSONResponse
    from starlette_context import context
    from starlette_context.middleware import ContextMiddleware
    from starlette_context.errors import ContextDoesNotExistError
    
    app = FastAPI()
    app.add_middleware(ContextMiddleware)
    
    @app.exception_handler(HTTPException)
    async def http_exception_handler(request: Request, exc: HTTPException):
        # Safely access context in exception handler
        extra_data = {}
        try:
            if context.exists():
                extra_data = {"request_id": context.get("X-Request-ID")}
        except ContextDoesNotExistError:
            pass
    
        return JSONResponse(
            status_code=exc.status_code,
            content={
                "message": exc.detail,
                **extra_data
            }
        )
  11. Enable context using Middleware

    master

    To make the context available during a standard web request, you must add the ContextMiddleware to your Starlette or FastAPI application. This manages the lifecycle of the context for every incoming request.

    from starlette.middleware import Middleware
    from starlette.applications import Starlette
    from starlette_context.middleware import ContextMiddleware
    
    app = Starlette(middleware=[Middleware(ContextMiddleware)])