The middleware pipeline
Every request Quiote handles flows through one PSR-15 middleware pipeline — an ordered list of small, single-purpose objects that each get a turn to touch the request on the way in and the response on the way out. Routing, security, validation, CSRF, and dispatch are not hidden framework magic; they are individual middleware in this list, and you can print the whole thing with MiddlewarePipeline::debugStack().
This page is the map of that pipeline: what’s in it, in what order, what each piece does, and how to add to or turn off parts of it. If you’ve used Agavi, this replaces the old global/action filter chain with a single flat list run by Relay.
How it fits in a request
Section titled “How it fits in a request”The pipeline is the request lifecycle — this is the canonical picture the rest of the docs link back to. Two things happen:
-
How the framework builds it. At worker start,
MiddlewareAttributeScannerfinds every class carrying a#[Middleware(phase, priority, before, after, enabled)]attribute.MiddlewareOrderResolverthen sorts them into a single order (the rules are in How the order is decided below).MiddlewarePipeline::doBuild()wraps that ordered list in a Relay chain and appends a terminal sentinel that throws if execution ever reaches the end without a response. -
The path a request takes. A middleware runs its “before” logic, calls the next one, and gets control back to run its “after” logic — so the list runs outermost-first inward, then unwinds:
Worker boots and builds the pipeline → request enters
StealthMiddleware(outermost) →ErrorHandlingMiddleware→SessionMiddlewarestarts the session →PayloadParsingMiddlewareparses the body →RoutingMiddlewareresolves the action → CSRF andSecurityMiddlewarecheck the request →ValidationMiddlewarevalidates input →DispatchMiddlewareruns the action and renders the view → the response unwinds back out through each middleware (CSRF token injection, form repopulation, timing) → response is emitted.
For the full request lifecycle including the kernel and emitter, see Request lifecycle.
The stack, in order
Section titled “The stack, in order”Middlewares run outermost-first on the way in, and unwind on the way out. The default order is:
StealthMiddleware ← outermost; strips identifying headers off the way outErrorHandlingMiddleware ← catches everything below itTelemetryMiddleware ← root request span + resource metrics (no-op when telemetry off)SessionMiddlewareTimingMiddlewareTraceMiddlewarePayloadParsingMiddlewareContentNegotiationMiddlewareRoutingMiddleware ← route is known after thisOutputTypeSyncMiddlewareCsrfInjectionMiddlewareCsrfValidationMiddlewareSecurityMiddleware ← authentication / authorizationValidationMiddlewareSlotMiddlewareDispatchMiddleware ← runs the action (effectively terminal)AssetAggregationMiddlewareFormPopulationMiddlewareExecutionTimeMiddlewareThe list above is not hand-maintained — it’s what the ordering attributes currently resolve to. Application middleware can join the same ordering pass; see Writing custom middleware.
How the order is decided
Section titled “How the order is decided”Each middleware carries a #[Middleware(phase:, priority:, before:, after:)] attribute, and MiddlewareOrderResolver turns those attributes into the single order above using two kinds of rule:
-
Phases are coarse buckets that always sort in this fixed sequence:
bootstrap → pre_routing → pre → routing → before_action → action → after_action → finalizeA middleware’s
phasedecides which bucket it lands in. Everything inbootstrapruns before anything inrouting, and so on — regardless of priority. -
prioritybreaks ties within a phase (higher priority runs first). Scan order is the final tie-breaker. -
before:/after:are hard placement constraints (topological edges) naming another middleware class. Use these when you must cross a phase boundary — for example, run ahead of a middleware that sits in an earlier phase than yours, which priority alone can never do.
After the attribute-ordered stack, any middleware added imperatively with MiddlewareCatalog::register() is spliced into its requested position, and the terminal sentinel is appended last.
Where the auth packages insert themselves
Section titled “Where the auth packages insert themselves”The optional quioteframework/auth package contributes two more middleware, both a no-op until an app registers a populated FirewallMap (see Authenticating with the auth packages):
StatelessAuthenticationMiddleware(HTTP Basic/bearer) — registeredbefore: SessionMiddleware, so it runs ahead of session start.SessionAuthenticationMiddleware(form login) — registeredafter: RoutingMiddleware, before: SecurityMiddleware.
Both use explicit before:/after: anchors rather than phase/priority tuning, because phase alone can’t place something ahead of SessionMiddleware: bootstrap (where SessionMiddleware sits) always sorts ahead of the later phases regardless of priority, so an anchor is the only way to guarantee an order that crosses a phase boundary. This is a general rule, not specific to authentication — see Writing custom middleware.
What each middleware does
Section titled “What each middleware does”StealthMiddleware — the outermost wrapper, and the last code to touch the response. With core.stealth_mode on it strips every X-Quiote-* header plus the names in core.stealth_additional_headers (X-Powered-By by default); with it off it is a pass-through. It sits outside the error handler so error and 404 responses are covered too. See the Middleware reference.
ErrorHandlingMiddleware — the outermost wrapper around request handling. Catches any Throwable from anything below it and renders a response (a developer error page when core.developer_exceptions is on, a safe generic page otherwise). It also logs the failure with request context.
TelemetryMiddleware — when telemetry is enabled, opens the root request span and records the resource metrics (time, CPU, memory); a pass-through no-op when off. See Telemetry.
SessionMiddleware — loads or creates this request’s session and installs it on the context as the session bag, so downstream middlewares (CSRF, security) and the action all reach the same session. On the way out it persists the user, then the session, and bakes the Set-Cookie onto the response.
TimingMiddleware / TraceMiddleware — optional diagnostics, both on by default and common to turn off in production (see Enabling and disabling middlewares). TimingMiddleware records total request time; TraceMiddleware records a running trace of executed middleware. Their response headers are off by default and configured through ordinary settings.* keys (middleware.timing.emit_header, middleware.trace.emit_header/header_name) — see the Middleware reference.
PayloadParsingMiddleware — parses the request body: JSON bodies and form bodies become the parsed body / parameters that validation and actions read.
ContentNegotiationMiddleware — decides the output type (html, json, …) from the Accept header, and records it on the request. Runs before routing so routing can still override it. See Output types.
RoutingMiddleware — matches the path against the routing table, extracts _module / _action, maps the HTTP verb to an action-method token, and builds an ActionDescriptor. After this, the target action is known.
OutputTypeSyncMiddleware — reconciles the output type chosen by negotiation with any output type fixed by the matched route.
CsrfInjectionMiddleware — wraps the response to inject CSRF tokens into rendered HTML forms and to set the readable XSRF-TOKEN cookie for SPA clients. Placed early so it post-processes the final HTML on the way out.
CsrfValidationMiddleware — on unsafe methods (non GET/HEAD/OPTIONS/TRACE), rejects the request with 403 unless a valid token is present. Per-route opt-out via _csrf => false. Both CSRF middlewares are gated by core.csrf.enabled.
SecurityMiddleware — authentication and authorization. For a secure action, it checks the user is authenticated and holds the required credentials, and forwards to the login or “secure” action otherwise. See Authentication and authorization.
ValidationMiddleware — runs the action’s validators (compiled/PHP and XML) plus manual validate() methods, and records a ValidationDecision. On failure it renders the action’s error view (or an RFC 9457 problem document for JSON) with a 400. See Validation.
SlotMiddleware — prepares sub-action (“slot”) rendering, so a view can embed the output of another action.
DispatchMiddleware — the core. Runs the action through ActionExecutor, renders the view, and builds the PSR-7 response. Effectively terminal — nothing downstream runs the action again.
AssetAggregationMiddleware — post-processes the response to aggregate/rewrite asset references.
FormPopulationMiddleware — repopulates submitted form field values into the rendered HTML on validation failure, so the user does not lose their input.
ExecutionTimeMiddleware — optional; records total execution time, added last so it measures the whole stack.
Enabling and disabling middlewares
Section titled “Enabling and disabling middlewares”Any middleware in the pipeline — framework or application — can be switched off. The default on/off state comes from each middleware’s #[Middleware(enabled:)] attribute (every framework middleware ships on); you override that default in a dedicated middleware.{xml,php,yaml,yml} config file, read by MiddlewareConfigHandler.
To disable a middleware, add a <use> entry for its class with enabled set to false:
return [ ['class' => \Quiote\Middleware\TimingMiddleware::class, 'enabled' => false],];- class: Quiote\Middleware\TimingMiddleware enabled: false<ae:configurations xmlns:ae="http://quiote.dev/quiote/config/global/envelope/1.1" xmlns="http://quiote.dev/quiote/config/parts/middleware/1.1"> <ae:configuration> <use class="Quiote\Middleware\TimingMiddleware" enabled="false" /> </ae:configuration></ae:configurations>A middleware without an entry keeps its attribute’s enabled default (all framework middleware default to on). A disabled middleware is never constructed.
The same <use> entry can carry phase, priority, before, and after to place a middleware, and override-framework (XML) / override_framework (PHP/YAML) to override the placement of a framework middleware — so this one file is also how apps and modules add middleware declaratively. See Writing custom middleware and the full schema in the Middleware reference.
Injecting your own middleware
Section titled “Injecting your own middleware”Applications add their own middleware three ways:
-
Declaratively, in
middleware.{xml,php,yaml,yml}— no code required. Because any module’sConfig/directory can carry this file, it’s also the only way a module registers middleware just by being present. Each entry is a<use>with the same fields as the enable/disable form above:Config/middleware.php return [['class' => \App\Middleware\RequestIdMiddleware::class, 'phase' => 'bootstrap', 'priority' => 80],];Config/middleware.yaml - class: App\Middleware\RequestIdMiddlewarephase: bootstrappriority: 80Config/middleware.xml <ae:configurations xmlns:ae="http://quiote.dev/quiote/config/global/envelope/1.1"xmlns="http://quiote.dev/quiote/config/parts/middleware/1.1"><ae:configuration><use class="App\Middleware\RequestIdMiddleware" phase="bootstrap" priority="80" /></ae:configuration></ae:configurations> -
The
#[Middleware]attribute plusMiddlewareCatalog::registerAttributed()— your class joins the framework’s ordering pass and is placed by its own attribute. -
MiddlewareCatalog::register()— register a middleware imperatively; it’s spliced into the built stack afterwards (with nobefore/after, it lands right afterValidationMiddleware).
All three are covered end to end — including where to register and the ordering caveats — in Writing custom middleware.
For the rare case where an app cannot run inside this lifecycle at all, MiddlewareCatalog::replaceCoreStack() discards the entire default stack and lets you supply your own — a deliberate footgun, gated behind an exact acknowledgement string. See Replacing the entire stack.
Inspecting the built pipeline
Section titled “Inspecting the built pipeline”MiddlewarePipeline::debugStack() returns the ordered list of labels for the pipeline as actually built, including your registered middlewares. It is the reliable way to confirm ordering in a test rather than reasoning about it.