Middleware Architecture
Framework M processes every HTTP request through a single, ordered ASGI middleware stack. This document explains why the stack is designed as a pluggable registry and how the pieces fit together.
Why a registry?
In monolithic web frameworks, middleware is typically declared in one place: the application factory. That works until multiple apps, plugins, and deployment environments all want to add, remove, or reorder middleware. The result is a central file that every feature touches.
Framework M avoids this by splitting the problem into three layers:
- Middleware modules implement the actual request/response logic.
- Entry points let packages contribute middleware without touching core code.
- The registry collects everything, applies deployment overrides, and sorts by priority.
This means a new capability like "recent document history" can be shipped as a single middleware module plus a single line in pyproject.toml.
How the stack is built
When create_app() runs, it:
- Clears the singleton
MiddlewareRegistryso repeated app construction in tests is deterministic. - Registers built-in middlewares that need runtime-constructed dependencies (rate limiting, CSRF, idempotency, auth, federated redirect, locale, metrics).
- Calls
create_middleware_stack(), which:- Discovers additional middlewares from
framework_m.middlewareentry points. - Applies
[web.middleware]config overrides. - Sorts all middlewares by priority.
- Wraps each in
DefineMiddleware(unless it is already aDefineMiddlewareinstance).
- Discovers additional middlewares from
The final list is passed to Litestar.
Priority ordering
Middleware priority follows the convention that lower values run first, which means they sit in the outer layer of the onion and wrap the handlers that run later.
| Band | Responsibility |
|---|---|
| 0–50 | Security and early processing (rate limiting, CSRF) |
| 50–100 | Authentication and identity |
| 100–150 | General framework features (recent document tracking) |
| 150–200 | Compression and response transformation |
| 200+ | Observability and tracing |
This ordering ensures, for example, that authentication resolves before recent-document tracking, so the middleware can read the current user.
Override semantics
The registry supports two override mechanisms:
Entry point name collision
If two packages register middleware with the same entry point name, the last one discovered wins. This is the same "last-wins" rule used for bootstrap steps and adapter overrides.
[project.entry-points."framework_m.middleware"]
recent_document = "my_app.middleware:MyRecentDocumentMiddleware"
Config-driven overrides
Deployments can tweak a middleware without writing Python:
[web.middleware.recent_document]
enabled = false
priority = 130
This is useful for disabling a feature in a specific environment or reordering middleware when a custom plugin needs to run at a specific point.
Built-in vs. contributed middleware
Built-in middlewares that need complex runtime setup (auth chain, CSRF adapter, rate-limit store) are registered in create_app() because their dependencies are constructed there. Simpler middlewares can be contributed entirely through entry points.
The boundary is intentional: the registry is the public extension point, and create_app() only contains the wiring that cannot be expressed declaratively.