Middleware

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

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. cookie_value(scope, name) reads one cookie off it, pair by pair, so a malformed sibling cookie never costs the request the cookies that are well-formed.

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.cookie_value(scope, name)[source]

The value of the name cookie on the request, or None.

Parsed pair by pair: a whole-header SimpleCookie.load raises CookieError on the first cookie with an illegal key (third-party trackers ship them), losing every well-formed sibling with it.

Return type:

str | 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])

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.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

get_middleware(middleware_class)[source]

The layer of that class in this chain, or None when it is off.

Parameters:

middleware_class (type[BaseMiddleware]) – the class to look for; a subclass of it answers too.

Return type:

BaseMiddleware | None

Returns:

The assembled instance, or None — a middleware nobody enabled has none.

The chain is walked from its head: every layer holds the next one, so the instances need not be kept a second time. What reads this is code that must do for a websocket what a middleware does for HTTP — the handshake asking SessionMiddleware for the session of a scope the chain never saw.

genro_asgi.middleware.default_registry()[source]

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

Return type:

dict[str, type[BaseMiddleware]]

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 times COOKIE_LIFETIME_FACTOR). The cookie is deliberately much longer than the session: the server-side TTL is SLIDING (every request refreshes last_access) while Max-Age is fixed from issue time, so a same-length cookie would log an active user out on schedule. The wide fixed cookie is the legacy-proven answer (GnrWebConnection.write_cookie: timeout × 24, never re-issued per request) — the server stays the only arbiter of expiry and no response but the first carries a Set-Cookie. 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

get_session(scope)[source]

The session the store holds for this scope’s cookie, or None.

Parameters:

scope (MutableMapping[str, Any]) – any scope carrying cookies — an HTTP request, or the handshake of a websocket, which the chain never sees.

Return type:

Any | None

Returns:

The stored session, or None when no cookie arrived or the store has nothing for it.

A pure reading: nothing is created here. The anonymous session of a first visit is born in __call__, which is where a cookie can be issued for it — a websocket handshake has no such moment to offer.