Skip to main content

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:

  1. Middleware modules implement the actual request/response logic.
  2. Entry points let packages contribute middleware without touching core code.
  3. 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:

  1. Clears the singleton MiddlewareRegistry so repeated app construction in tests is deterministic.
  2. Registers built-in middlewares that need runtime-constructed dependencies (rate limiting, CSRF, idempotency, auth, federated redirect, locale, metrics).
  3. Calls create_middleware_stack(), which:
    • Discovers additional middlewares from framework_m.middleware entry points.
    • Applies [web.middleware] config overrides.
    • Sorts all middlewares by priority.
    • Wraps each in DefineMiddleware (unless it is already a DefineMiddleware instance).

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.

BandResponsibility
0–50Security and early processing (rate limiting, CSRF)
50–100Authentication and identity
100–150General framework features (recent document tracking)
150–200Compression 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.