Browse documentation

Documentation overview

Tutorial

Reference

Project

Write a little ASGI middleware

Sometimes you need a request ID, a timing header, or a place to attach context. This does not require founding another framework.

Three arguments, two directions

An ASGI app is called with (scope, receive, send). The scope describes the connection. receive() supplies request events; send() emits response events. Middleware can wrap either callable and then delegate to the inner app.

This example assigns a request ID, makes it available to handlers, and adds response headers when http.response.start is sent. It measures time until response headers, not the complete download time. Names are useful when they mean things.

Add request context and timing

Save as context_app.py, then run weft context_app:app.

context_app.pyDownload
from time import perf_counter
from uuid import uuid4

from wred import App


class RequestContext:
    def __init__(self, app):
        self.app = app

    async def __call__(self, scope, receive, send):
        if scope['type'] != 'http':
            return await self.app(scope, receive, send)

        request_id = uuid4().hex
        started = perf_counter()
        scope = {**scope, 'state': {**scope.get('state', {}), 'request_id': request_id}}

        async def send_with_context(message):
            if message['type'] == 'http.response.start':
                elapsed_ms = (perf_counter() - started) * 1000
                message = {
                    **message,
                    'headers': [
                        *message.get('headers', []),
                        (b'x-request-id', request_id.encode('ascii')),
                        (b'server-timing', f'app;dur={elapsed_ms:.2f}'.encode('ascii')),
                    ],
                }
            await send(message)

        await self.app(scope, receive, send_with_context)


api = App()


@api.route('/api/hello')
async def hello(request):
    return {'request_id': request.scope['state']['request_id']}


app = RequestContext(api)
Terminal
curl -i http://127.0.0.1:8000/api/hello

The body’s request_id matches X-Request-ID. Server-Timing reports the elapsed milliseconds up to response start. Each request keeps its own variables inside __call__; shared instance attributes would invite concurrent requests to exchange identities.

Keep the wrapper well behaved

Our wrapper leaves the body alone. It also passes lifespan through rather than implementing it; wrapping a bare Wred app still leaves you with an HTTP-only app.

The pure ASGI middleware guide describes this pattern. Middleware can also come from other packages: the interoperability is in the ASGI call, not in matching logos.

Use Uvicorn’s proxy-header middleware with another server

You can reuse Uvicorn’s ProxyHeadersMiddleware while serving with Weft. Install uvicorn in addition to the notebook’s dependencies, save this as proxy_app.py at the repository root, and run weft proxy_app:app --host 127.0.0.1 --lifespan on.

proxy_app.pyDownload
from uvicorn.middleware.proxy_headers import ProxyHeadersMiddleware

from example_app.app import create_app

# Use only behind a trusted proxy connecting from IPv4 loopback.
app = ProxyHeadersMiddleware(create_app(), trusted_hosts=['127.0.0.1'])

It accepts forwarded scheme and client information only from the listed proxy peers. For this example, the reverse proxy must connect over IPv4 loopback. Keep the backend private; a client that reaches it directly from a trusted address can supply those headers too.

This is an alternative to Weft’s --forwarded-allow-ips option used in the deployment chapters. Pick one owner for proxy-header processing. See the Uvicorn implementation.

Authentication middleware can use the same scope

Starlette’s authentication middleware places authentication information in scope["user"] and scope["auth"]. A Wred handler can read those keys directly. The middleware’s authentication backend and your authorization rules remain application choices.

The notebook uses its own @protected decorator instead. That decorator adds request.user itself. Don’t assume the property exists in unrelated Wred apps, or that wrapping with authentication middleware automatically protects every route.