Middleware

The middleware base and mixin, and the shipped middleware: errors, well-known, logging, CORS, authentication, session.

Middleware capability: the chain as a mixin over the base server (D16).

The base server has NO middleware. This mixin adds the chain as a capability, composed BEFORE the server class (class MyServer(MiddlewareMixin, BaseServer)): its cooperative __init__ peels middleware= (the {name: bool | dict} switches; None arms only the defaults) and middleware_registry= (extra {name: class} entries merged over default_registry()), builds the chain ONCE around _base_call — the adapter delegating to the next __call__ in the MRO, i.e. the base dispatch — and routes ONLY http scopes through it: lifespan and websocket go straight to super().__call__. A composition WITHOUT the mixin simply lacks the attributes — a different type, not a ghost.

default_registry() returns a FRESH dict per call ({“errors”: ErrorMiddleware, “wellknown”: WellKnownMiddleware, “logging”: LoggingMiddleware, “cors”: CORSMiddleware, “auth”: AuthMiddleware, “session”: SessionMiddleware} as of Phase 5) — deliberately a function so no module-level mutable registry exists. It lives in this module, not in base.py, because base.py cannot import the concrete middleware modules (which subclass BaseMiddleware) without a cycle.

class genro_asgi.middleware.AuthMiddleware(app, server, **options)[source]

Bases: BaseMiddleware

Sets scope["auth"] from the server’s identity resolution.

Parameters:
  • app (ASGIApp)

  • server (Any)

  • options (Any)

class genro_asgi.middleware.BaseMiddleware(app, server, **options)[source]

Bases: object

Base class for chain middlewares: holds the next app and the server.

Class attributes:

middleware_order: position in the chain (lower = outermost). middleware_default: on/off state when the config does not name it.

Parameters:
  • app (ASGIApp)

  • server (Any)

  • options (Any)

property app: Callable[[MutableMapping[str, Any], Callable[[], Awaitable[MutableMapping[str, Any]]], Callable[[MutableMapping[str, Any]], Awaitable[None]]], Awaitable[None]]

The next ASGI callable in the chain (towards the base dispatch).

property server: Any

The owning server, handed in at construction — never walked to.

property logger: Logger

This middleware’s instance logger.

class genro_asgi.middleware.CORSMiddleware(app, server, allow_origins=None, allow_methods=None, allow_headers=None, allow_credentials=False, expose_headers=None, max_age=600, **options)[source]

Bases: BaseMiddleware

Answer CORS preflight requests and add CORS headers to responses.

Parameters:
  • app (ASGIApp)

  • server (Any)

  • allow_origins (str | list[str] | None)

  • allow_methods (str | list[str] | None)

  • allow_headers (str | list[str] | None)

  • allow_credentials (bool)

  • expose_headers (str | list[str] | None)

  • max_age (int)

  • options (Any)

class genro_asgi.middleware.ErrorMiddleware(app, server, **options)[source]

Bases: BaseMiddleware

Outermost middleware answering raised exceptions with HTTP responses.

Parameters:
  • app (ASGIApp)

  • server (Any)

  • options (Any)

class genro_asgi.middleware.LoggingMiddleware(app, server, level='INFO', include_headers=False, include_query=True, **options)[source]

Bases: BaseMiddleware

Log request arrival and response completion with timing.

Parameters:
  • app (ASGIApp)

  • server (Any)

  • level (str)

  • include_headers (bool)

  • include_query (bool)

  • options (Any)

class genro_asgi.middleware.MiddlewareMixin(**kwargs)[source]

Bases: object

Middleware capability mixin, composed BEFORE a server class.

Constructor kwargs peeled here: middleware — the {name: bool | dict} switches (a dict value enables the middleware and becomes its constructor options); middleware_registry — extra {name: class} entries merged over default_registry().

Parameters:

kwargs (Any)

property middleware_chain: Callable[[MutableMapping[str, Any], Callable[[], Awaitable[MutableMapping[str, Any]]], Callable[[MutableMapping[str, Any]], Awaitable[None]]], Awaitable[None]]

outermost middleware first, base dispatch innermost.

Type:

The assembled chain

class genro_asgi.middleware.SessionMiddleware(app, server, cookie_name='session_id', secure=False, samesite='lax', **options)[source]

Bases: BaseMiddleware

Per-server session middleware: cookie in, session on the scope, cookie out.

Parameters:
  • app (ASGIApp)

  • server (Any)

  • cookie_name (str)

  • secure (bool)

  • samesite (str)

  • options (Any)

__init__(app, server, cookie_name='session_id', secure=False, samesite='lax', **options)[source]

Store the cookie configuration; server supplies session_store.

Parameters:
Return type:

None

class genro_asgi.middleware.WellKnownMiddleware(app, server, **options)[source]

Bases: BaseMiddleware

Raise 404 for well-known/probe paths; delegate everything else.

Parameters:
  • app (ASGIApp)

  • server (Any)

  • options (Any)

genro_asgi.middleware.build_chain(config, innermost, server, registry)[source]

Assemble the middleware chain around innermost and return its head.

Every middleware named by config must exist in registry (ValueError otherwise); registry entries the config does not name follow their middleware_default. A dict config value enables the middleware and becomes its constructor options.

Return type:

Callable[[MutableMapping[str, Any], Callable[[], Awaitable[MutableMapping[str, Any]]], Callable[[MutableMapping[str, Any]], Awaitable[None]]], Awaitable[None]]

Parameters:
genro_asgi.middleware.default_registry()[source]

A fresh {name: class} mapping of the middlewares shipped with the core.

Return type:

dict[str, type[BaseMiddleware]]

genro_asgi.middleware.headers_dict(scope)[source]

Parse scope headers into a lowercase-keyed dict, cached as scope["_headers"].

Names and values are decoded as latin-1 per the ASGI spec; duplicate headers collapse to the last value.

Return type:

dict[str, str]

Parameters:

scope (MutableMapping[str, Any])

Middleware base class and the chain mechanism.

BaseMiddleware(app, server, **options) receives BOTH ends at construction (dual parent-child): app is the next ASGI callable in the chain, server the owning server — never discovered by walking wrappers. Subclasses declare middleware_order (lower = outermost; errors 100, logging 200, security 300, auth 400, business 500-800, transformation 900) and middleware_default (their on/off state when the config does not name them).

build_chain(config, innermost, server, registry) assembles the chain from explicit inputs — config maps {name: bool | dict} (a dict value means “on” and becomes the middleware’s constructor options), registry maps {name: class} and is always passed in: there is NO module-level registry and no import-time registration anywhere. Enabled middlewares are sorted by middleware_order and wrapped innermost-out, so the lowest order ends up outermost. A config name missing from the registry raises ValueError.

headers_dict(scope) parses the ASGI headers into a lowercase-keyed dict cached as scope["_headers"] — the one header-parse shared by the session and auth middlewares downstream.

class genro_asgi.middleware.base.BaseMiddleware(app, server, **options)[source]

Bases: object

Base class for chain middlewares: holds the next app and the server.

Class attributes:

middleware_order: position in the chain (lower = outermost). middleware_default: on/off state when the config does not name it.

Parameters:
  • app (ASGIApp)

  • server (Any)

  • options (Any)

property app: Callable[[MutableMapping[str, Any], Callable[[], Awaitable[MutableMapping[str, Any]]], Callable[[MutableMapping[str, Any]], Awaitable[None]]], Awaitable[None]]

The next ASGI callable in the chain (towards the base dispatch).

property server: Any

The owning server, handed in at construction — never walked to.

property logger: Logger

This middleware’s instance logger.

genro_asgi.middleware.base.build_chain(config, innermost, server, registry)[source]

Assemble the middleware chain around innermost and return its head.

Every middleware named by config must exist in registry (ValueError otherwise); registry entries the config does not name follow their middleware_default. A dict config value enables the middleware and becomes its constructor options.

Return type:

Callable[[MutableMapping[str, Any], Callable[[], Awaitable[MutableMapping[str, Any]]], Callable[[MutableMapping[str, Any]], Awaitable[None]]], Awaitable[None]]

Parameters:
genro_asgi.middleware.base.headers_dict(scope)[source]

Parse scope headers into a lowercase-keyed dict, cached as scope["_headers"].

Names and values are decoded as latin-1 per the ASGI spec; duplicate headers collapse to the last value.

Return type:

dict[str, str]

Parameters:

scope (MutableMapping[str, Any])

Error middleware: the outermost try/except of the chain and the login seam.

ErrorMiddleware (order 100, the only middleware enabled by default — errors=False disables it) maps control-flow exceptions to responses: Redirect → its status plus the Location header, HTTPException → its status with the detail, any other Exception → a hidden 500 logged via the instance logger. Responses are built with the Response class; an exception’s headers (e.g. a WWW-Authenticate challenge) are forwarded onto the response.

Content negotiation (D4 error-body reconciliation): the error body follows the caller’s Accept. A caller asking for JSON (application/json or */*, never text/html) gets the {"error": ...} document built by Response.set_error — the single live JSON error path; anyone else keeps the historical text/plain body. A missing Accept stays text/plain (the pre-existing default).

Challenge negotiation (only when the server carries an active login surface — server.login_enabled): a 401 is where the server asks the caller to authenticate. A browser NAVIGATION (an http GET whose Accept includes text/html) gets a 302 to /_server/login_page carrying the original path+query as a safe_next_path-validated next; any other caller keeps the bare 401 (with its WWW-Authenticate) and gains a {"login_url": ...} JSON body so an SPA can drive the login. With the login surface off the 401 is answered exactly like any other error. The request shape is read from the scope headers (headers_dict) — never an ambient request.

The middleware wraps send to track whether http.response.start has already passed downstream: an exception raised AFTER the response started cannot be answered (a second start would corrupt the stream), so it is logged and re-raised — the server/transport tears the connection down. The chain only carries http scopes (the mixin routes the others past it), so no scope filtering happens here.

class genro_asgi.middleware.errors.ErrorMiddleware(app, server, **options)[source]

Bases: BaseMiddleware

Outermost middleware answering raised exceptions with HTTP responses.

Parameters:
  • app (ASGIApp)

  • server (Any)

  • options (Any)

Well-known / probe path filter.

Browsers and bots probe a handful of conventional paths on every site (/.well-known/* per RFC 8615, /robots.txt, /sitemap.xml). When the site does not expose them, this middleware answers with a clean 404 instead of letting the probe reach the mounted application.

class genro_asgi.middleware.wellknown.WellKnownMiddleware(app, server, **options)[source]

Bases: BaseMiddleware

Raise 404 for well-known/probe paths; delegate everything else.

Parameters:
  • app (ASGIApp)

  • server (Any)

  • options (Any)

HTTP access logging middleware.

Logs each request’s arrival and completion (method, path, status, timing) through the instance logger inherited from BaseMiddleware — never a module-level logger.

class genro_asgi.middleware.logging.LoggingMiddleware(app, server, level='INFO', include_headers=False, include_query=True, **options)[source]

Bases: BaseMiddleware

Log request arrival and response completion with timing.

Parameters:
  • app (ASGIApp)

  • server (Any)

  • level (str)

  • include_headers (bool)

  • include_query (bool)

  • options (Any)

CORS (Cross-Origin Resource Sharing) middleware.

Adds CORS headers to HTTP responses and answers preflight OPTIONS requests. Preflight-only headers are precomputed once in __init__.

class genro_asgi.middleware.cors.CORSMiddleware(app, server, allow_origins=None, allow_methods=None, allow_headers=None, allow_credentials=False, expose_headers=None, max_age=600, **options)[source]

Bases: BaseMiddleware

Answer CORS preflight requests and add CORS headers to responses.

Parameters:
  • app (ASGIApp)

  • server (Any)

  • allow_origins (str | list[str] | None)

  • allow_methods (str | list[str] | None)

  • allow_headers (str | list[str] | None)

  • allow_credentials (bool)

  • expose_headers (str | list[str] | None)

  • max_age (int)

  • options (Any)

Authentication middleware — publishes the request identity on the scope.

Delegates the whole verdict to server.authenticate(scope) (the §5.5 precedence living on AuthMixin) and stores the result on scope["auth"] for downstream handlers. A present-but-invalid credential raises HTTPUnauthorized from the auth core, caught outermost by ErrorMiddleware (order 100) and turned into a 401. Armed by AuthMixin; order 450 (INSIDE SessionMiddleware at 400, so scope["session"] is already attached and the §5.5 session fallback is live), default OFF. The chain only carries http scopes, so no scope filtering happens here.

class genro_asgi.middleware.authentication.AuthMiddleware(app, server, **options)[source]

Bases: BaseMiddleware

Sets scope["auth"] from the server’s identity resolution.

Parameters:
  • app (ASGIApp)

  • server (Any)

  • options (Any)

Session middleware — session lifecycle driven by the request cookie.

Reads the session token from the request Cookie header (via the shared headers_dict scope cache, not a request object), reconnects an existing session or creates a new ANONYMOUS one through server.session_store (store.create() with no avatar — capturing an identity into a session is an explicit act of the login surface, core 1d), attaches it to scope["session"], and — ONLY when the session was created here — wraps send to add its Set-Cookie header (HttpOnly, Max-Age = the session TTL). Login never changes the session id: a handler attaches the avatar to the existing session in place (request.session.attach_avatar), so the cookie the client already holds stays valid and no login-time cookie exists — handlers stay pure and never set cookies themselves. Armed by SessionMixin; order 400 (OUTSIDE AuthMiddleware at 450, so the session is on the scope before the §5.5 fallback runs), default OFF. The chain only carries http scopes, so no scope filtering happens here.

class genro_asgi.middleware.session.SessionMiddleware(app, server, cookie_name='session_id', secure=False, samesite='lax', **options)[source]

Bases: BaseMiddleware

Per-server session middleware: cookie in, session on the scope, cookie out.

Parameters:
  • app (ASGIApp)

  • server (Any)

  • cookie_name (str)

  • secure (bool)

  • samesite (str)

  • options (Any)

__init__(app, server, cookie_name='session_id', secure=False, samesite='lax', **options)[source]

Store the cookie configuration; server supplies session_store.

Parameters:
Return type:

None