Browse documentation

Documentation overview

Tutorial

Reference

Project

Borrow Starlette’s middleware

ASGI middleware wraps an ASGI app. Wred is an ASGI app. No adapter ceremony, secret handshake, or framework conversion experience required.

Install what you use

Terminal
python -m pip install 'wred[server]' starlette itsdangerous

itsdangerous is needed for the session recipe below. Wred’s request is still a Wred request after wrapping; middleware-provided data is available in its ASGI scope.

Build an explicit stack

Save this as starlette_app.py and run weft starlette_app:app. Register routes on api; export the completed middleware stack as app.

starlette_app.pyDownload
from starlette.middleware.cors import CORSMiddleware
from starlette.middleware.errors import ServerErrorMiddleware
from starlette.middleware.gzip import GZipMiddleware
from starlette.middleware.trustedhost import TrustedHostMiddleware
from starlette.responses import JSONResponse
from wred import App

api = App()


@api.route('/api/hello')
async def hello(request):
    return {'hello': 'world'}


async def server_error(request, exc):
    return JSONResponse({'error': 'Internal server error'}, status_code=500)


# Requests enter the last wrapper first.
app = GZipMiddleware(api, minimum_size=1000)
app = TrustedHostMiddleware(app, allowed_hosts=['localhost', '127.0.0.1', 'api.example.com'])
app = ServerErrorMiddleware(app, handler=server_error, debug=False)
app = CORSMiddleware(
    app,
    allow_origins=['http://localhost:5173'],
    allow_methods=['GET', 'POST', 'PATCH', 'DELETE'],
    allow_headers=['Authorization', 'Content-Type'],
    expose_headers=['Location'],
)
CORSServer errorsTrusted hostGzipWred

The last wrapper receives the request first. Responses travel back through the layers. The wrapper accepting the request is the object your ASGI server must import. Exporting the inner api skips every layer, a very efficient way to misconfigure middleware.

CORS belongs outside the error handler

This sample allows a frontend at http://localhost:5173. CORS is outermost so generated 500 responses can carry its headers. It also answers browser preflight requests before they reach Wred.

For cookie credentials, set allow_credentials=True and explicit origins, methods, and headers. CORS grants browser access; it does not authenticate the caller.

See Starlette’s CORS documentation and implementation.

Terminal
curl -i -X OPTIONS http://127.0.0.1:8000/api/hello \
  -H 'Origin: http://localhost:5173' \
  -H 'Access-Control-Request-Method: POST' \
  -H 'Access-Control-Request-Headers: Content-Type'

Expect a successful preflight with access-control-allow-origin: http://localhost:5173. The actual sample endpoint is GET-only; a successful preflight does not create a POST handler. The route list still has a job.

Hosts, compression, and errors

TrustedHostMiddleware checks the Host header. It does not decide which proxy IPs to trust. Gzip compresses eligible responses above the configured threshold. Our tiny hello JSON is below 1,000 bytes, so it remains tiny hello JSON.

ServerErrorMiddleware supplies a JSON 500 response while allowing the exception to be logged. Debug mode stays off here; a public traceback is a surprisingly detailed résumé for your server.

See the middleware reference. If your reverse proxy handles compression, choose one compression layer. Configure HTTPS redirects only after the app receives the correct forwarded scheme; otherwise you may build a redirect loop with excellent uptime.

A separate signed-cookie session recipe

This is an alternative example, not a replacement for the notebook’s database-backed logins. Save as session_app.py.

session_app.pyDownload
import os

from starlette.middleware.sessions import SessionMiddleware
from wred import App

api = App()


@api.route('/visits')
async def visits(request):
    session = request.scope['session']
    session['visits'] = session.get('visits', 0) + 1
    return {'visits': session['visits']}


app = SessionMiddleware(
    api,
    secret_key=os.environ['SESSION_SECRET'],
    same_site='lax',
    https_only=os.environ.get('COOKIE_SECURE', '1') == '1',
)
Terminal
export SESSION_SECRET="$(python -c 'import secrets; print(secrets.token_hex(32))')"
# Local HTTP only. Leave COOKIE_SECURE unset for HTTPS deployments.
COOKIE_SECURE=0 weft session_app:app

In a second terminal:

Terminal
curl -sS -c visits.cookies http://127.0.0.1:8000/visits
curl -sS -b visits.cookies -c visits.cookies http://127.0.0.1:8000/visits
# {"visits":2}

Keep the secret stable across workers and restarts. Session data is signed, not encrypted: store a harmless preference or counter, not a password. Wred reads it through request.scope["session"]. There is no request.session property waiting to be discovered.

See Starlette’s session implementation.