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:
BaseMiddlewareSets
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:
objectBase 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)
- 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:
BaseMiddlewareAnswer CORS preflight requests and add CORS headers to responses.
- class genro_asgi.middleware.ErrorMiddleware(app, server, **options)[source]
Bases:
BaseMiddlewareOutermost 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:
BaseMiddlewareLog request arrival and response completion with timing.
- class genro_asgi.middleware.MiddlewareMixin(**kwargs)[source]
Bases:
objectMiddleware 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 overdefault_registry().- Parameters:
kwargs (Any)
- class genro_asgi.middleware.SessionMiddleware(app, server, cookie_name='session_id', secure=False, samesite='lax', **options)[source]
Bases:
BaseMiddlewarePer-server session middleware: cookie in, session on the scope, cookie out.
- class genro_asgi.middleware.WellKnownMiddleware(app, server, **options)[source]
Bases:
BaseMiddlewareRaise 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
innermostand return its head.Every middleware named by
configmust exist inregistry(ValueErrorotherwise); registry entries the config does not name follow theirmiddleware_default. A dict config value enables the middleware and becomes its constructor options.
- genro_asgi.middleware.default_registry()[source]
A fresh
{name: class}mapping of the middlewares shipped with the core.- Return type:
- 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.
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:
objectBase 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)
- genro_asgi.middleware.base.build_chain(config, innermost, server, registry)[source]
Assemble the middleware chain around
innermostand return its head.Every middleware named by
configmust exist inregistry(ValueErrorotherwise); registry entries the config does not name follow theirmiddleware_default. A dict config value enables the middleware and becomes its constructor options.
- 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.
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:
BaseMiddlewareOutermost 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:
BaseMiddlewareRaise 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:
BaseMiddlewareLog request arrival and response completion with timing.
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:
BaseMiddlewareAnswer CORS preflight requests and add CORS headers to responses.
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:
BaseMiddlewareSets
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:
BaseMiddlewarePer-server session middleware: cookie in, session on the scope, cookie out.