Overview

How Chirp's protocol-based middleware works — the function shape, the pipeline order, and how to short-circuit

Page actions AI-ready formats and sharing
Open LLM text
Share with AI
Ask Claude Ask ChatGPT Ask Gemini Ask Copilot

Overview

Middleware is code that runs around your route handlers — before the request reaches them and after they return a response. Reach for it when behavior belongs to every route (logging, auth gates, response headers) rather than to one handler.

In Chirp a middleware is an async function. There's no base class to subclass and no decorator to apply — you write a function matching one shape and register it:

from chirp import Next, Request, AnyResponse

async def my_middleware(request: Request, next: Next) -> AnyResponse:
    # before the handler runs
    response = await next(request)
    # after the handler returns
    return response

That is the entire contract: a callable that takes a Request and a Next, and returns a response.nextinvokes the rest of the pipeline — the next middleware, or the handler itself. Whatever a handler returns flows back through as a response, because in Chirp the return type is the intent.

The pipeline

Each middleware wraps the next, so a request passes through them on the way in and back out on the way out:

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

You register middleware in setup order:

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

Explicit ordering withpriority

When registration order is awkward (for example a plugin adds middleware you want to keep outermost regardless of where its setup runs), pass a keyword-only priorityso the order is explicit and independent of registration order:

app.add_middleware(timing_middleware, priority=-100)  # always outermost
app.add_middleware(cors_middleware)                    # priority 0 (default)
app.add_middleware(auth_middleware, priority=10)       # innermost of the three

Lower priority runs outermost. At freeze the user middleware is sorted by (priority, registration_order)with a stable sort, so equal-priority middleware keep their registration order and a stack that never passespriority is byte-identical to plain registration order. Built-in middleware (allowed-hosts, CSP nonce, security headers, injection) stays positionally pinned around your chain —priorityonly reorders middleware you register.

Apriority that would place CSRFMiddleware outside SessionMiddlewarestill raisesConfigurationError at freeze (CSRF reads the session). app.check() reports the resolved order under the INFOmiddleware_chaindiagnostic so you can confirm the pipeline you registered is the pipeline that runs.

What you can do in a middleware

Inside the function you control both halves of the round trip:

from chirp import Next, Request, AnyResponse

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

    response = await next(request)

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

From here you can:

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

A class with anasync def __call__(self, request, next)method matches the same shape, so stateful middleware (rate limiters, counters) is a callable object rather than a function.

Short-circuiting

Return a response without callingnextto block the request before it reaches any handler:

from chirp import Next, Request, AnyResponse, Response

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

.with_status() and .with_header()are chainable and return a new response, so you can adjust status and headers inline.