Middleware

Purpose

The middleware creates and manages the context for each request. It’s essential to configure your app to use one of the context middlewares to access the context data throughout the request lifecycle.

Detailed usage examples with plugins can be found in the Plugins section.

Available Middlewares

starlette-context provides two middleware implementations:

ContextMiddleware

The ContextMiddleware is built on Starlette’s BaseHTTPMiddleware. It’s simple to use and provides a clean interface for context management:

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)

RawContextMiddleware

The RawContextMiddleware operates at a lower ASGI level and avoids some of the limitations of BaseHTTPMiddleware. It’s particularly useful when working with streaming responses:

from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette_context.middleware import RawContextMiddleware
from starlette_context import plugins

middleware = [
    Middleware(
        RawContextMiddleware,
        plugins=(
            plugins.RequestIdPlugin(),
            plugins.CorrelationIdPlugin()
        )
    )
]

app = Starlette(middleware=middleware)

Middleware Order

The context middleware must be listed first in your middleware list. Starlette processes middlewares in reverse order, so the first middleware in the list wraps the outermost layer. Any middleware that needs to access context must appear after the context middleware:

middleware = [
    Middleware(ContextMiddleware),   # 1st — creates the context
    Middleware(LoggingMiddleware),   # 2nd — can use context
    Middleware(AuthMiddleware),      # 3rd — can use context
]

If a middleware is listed before ContextMiddleware, accessing context inside it will raise ContextDoesNotExistError. See Handling Errors for details.

Choosing the Right Middleware

Both middlewares provide the same core functionality, but have different implementations:

  • ContextMiddleware:

    • Simpler to understand and use

    • Built on Starlette’s BaseHTTPMiddleware

    • Works well for most use cases

  • RawContextMiddleware:

    • Works at a lower ASGI level

    • Better performance with streaming responses

    • Doesn’t have the same memory issues with large responses

For most applications, either middleware will work fine. If you’re using StreamingResponse or dealing with large responses, consider using RawContextMiddleware.

Error Handling in Middlewares

When a validation error occurs in a plugin during request processing, the middleware needs to return an error response. Starlette doesn’t allow middleware to use the regular error handler (see Starlette documentation), so the middleware has to send a response itself.

By default, the response will be a 400 status code with no body or extra headers (using Response(status_code=400)). You can customize this response at both middleware and plugin levels.

Custom Error Responses

You can provide a custom error response when initializing the middleware:

from starlette.responses import JSONResponse
from starlette.status import HTTP_422_UNPROCESSABLE_CONTENT

middleware = [
    Middleware(
        ContextMiddleware,
        default_error_response=JSONResponse(
            status_code=HTTP_422_UNPROCESSABLE_CONTENT,
            content={"error": "Invalid request"},
        ),
        plugins=(
            # your plugins...
        )
    )
]

Plugins can also provide their own error responses, which take precedence over the middleware’s default error response.

Exception Handling Limitation

Due to how Starlette handles application exceptions, the enrich_response method won’t run and the default error response won’t be used after an unhandled exception.

Therefore, these middlewares can’t set response headers for 500 responses. You can use your own 500 handler, but be aware that the context may not be available in exception handlers.

Middleware Mechanics

  1. The middleware creates an empty “storage” bound to the context of your async request

  2. The set_context method populates this storage with data from plugins

  3. When the response is created, plugins can add headers based on context values

  4. Finally, the request context is cleaned up

You can customize the context creation by either:

  • Using plugins to add data to the context

  • Overriding the middleware’s set_context method to add custom data

class CustomContextMiddleware(ContextMiddleware):
    async def set_context(self, request: Request) -> dict:
        # Get the standard context from plugins
        context = await super().set_context(request)

        # Add custom data
        context["environment"] = os.environ.get("ENVIRONMENT", "development")
        context["app_version"] = "1.0.0"

        return context