Overview

How Chirp's protocol-based middleware works

2 min read 329 words

No Base Class

Chirp middleware uses a Protocol, not inheritance. A middleware is anything that matches this shape:

async def my_middleware(request: Request, next: Next) -> Response:
    # Before the handler
    response = await next(request)
    # After the handler
    return response

That is the entire contract. A function that takes a Request and a Next callable, returns a Response.

The Pipeline

Middleware forms a pipeline. Each middleware wraps the next:

Request → Middleware A → Middleware B → Route Handler → Response
                                   ↩ Middleware B ↩ Middleware A → Client

Middleware registered first runs first on the way in and last on the way out.

Registration

app.add_middleware(timing_middleware)
app.add_middleware(cors_middleware)
app.add_middleware(auth_middleware)

Order matters. In this example, timing_middlewarewraps everything -- it sees the total time including CORS and auth processing.

The Next Callable

Nextis a callable that invokes the rest of the pipeline:

from chirp import Next

async def my_middleware(request: Request, next: Next) -> Response:
    # Runs before the handler
    print(f"Incoming: {request.method} {request.path}")

    # Call the next middleware (or the handler)
    response = await next(request)

    # Runs after the handler
    print(f"Response: {response.status}")
    return response

You can:

  • Modify the request before passing it tonext
  • Short-circuit by returning a response without callingnext
  • Modify the response afternextreturns
  • Catch exceptions fromnext

Short-Circuiting

Return early without callingnextto block the request:

async def auth_guard(request: Request, next: Next) -> Response:
    if not request.headers.get("Authorization"):
        return Response("Unauthorized").with_status(401)
    return await next(request)

Response Types

Middleware handles all response types through theAnyResponseunion:

from chirp import AnyResponse  # Response | StreamingResponse | SSEResponse

For most middleware, you only need to work with Response. Streaming and SSE responses pass through middleware but their bodies cannot be inspected synchronously.

Next Steps