Skip to content

Middleware reference

This is the lookup table for Quiote’s default pipeline. Every middleware that ships with the framework gets its own section, in execution order, covering what it does, the settings it reads, and what those settings affect. Reach for this page when you need the exact key or default for one middleware; for the big-picture “what runs when and why,” start with The middleware pipeline, and for adding your own, Writing custom middleware.

Each middleware plays a fixed role in handling a request — the pipeline builds itself at worker start by scanning #[Middleware] attributes and ordering them, then every request flows through the resolved list (StealthErrorHandlingSessionRouting → CSRF/SecurityValidationDispatch → back out). See How it fits in a request for the full chain.

Middleware draws configuration from four distinct places — this page is explicit about which applies to each:

  • core.* settings — read via Config::get('core.…') from your settings config (PHP/YAML/XML). Most tunables live here.
  • Other namespaced settings — a few middleware read a non-core. prefix instead (middleware.timing.*, middleware.trace.*, validation.*, routing.*); same settings config, just a different key prefix (see Configuration: The XML prefix attribute for the XML form).
  • Enable/disableany middleware can be switched off in the middleware.{xml,php,yaml,yml} config file (see Enabling and disabling below). Each middleware’s #[Middleware(enabled:)] attribute sets its default (all default to on); a <use ... enabled="false"> entry overrides it.
  • Environment variables — one middleware (PayloadParsingMiddleware) reads an OS env var.
  • Constructor arguments — some behaviour is only reachable by constructing the middleware yourself (a custom pipeline), with no config-file switch. Called out where it applies.

Every middleware defaults to on and can be disabled with a middleware.* <use ... enabled="false"> entry (see Enabling and disabling); this column lists only other configuration.

#MiddlewareConfigurable
1StealthMiddlewarecore.stealth_mode, core.stealth_additional_headers
2ErrorHandlingMiddlewarecore.developer_exceptions
3TelemetryMiddlewaretelemetry.* (see Telemetry)
4SessionMiddleware— (backend via the session role in factories)
5TimingMiddlewaremiddleware.timing.emit_header
6TraceMiddlewaremiddleware.trace.emit_header, middleware.trace.header_name
7PayloadParsingMiddlewareQUIOTE_JSON_STRICT env
8ContentNegotiationMiddleware
9RoutingMiddleware— (uses routing.http_method_map)
10OutputTypeSyncMiddleware
11CsrfInjectionMiddlewarecore.csrf.*
12CsrfValidationMiddlewarecore.csrf.*
13SecurityMiddlewarecore.use_security
StatelessAuthenticationMiddleware / SessionAuthenticationMiddleware (optional, quioteframework/auth)— (no-op until a FirewallMap is registered)
14ValidationMiddlewarecore.expose_validation_errors_header, validation.*
15SlotMiddleware
16DispatchMiddlewarecore.cache_enabled, core.use_cache, response-header keys
17AssetAggregationMiddleware
18FormPopulationMiddleware— (per-request state)
19ExecutionTimeMiddleware

All core.* and validation.*/routing.* keys can be written in any config format — see Configuration. Examples below use settings.php.


Outermost middleware (phase bootstrap, priority 1200). Strips framework-identifying headers off the response on the way out when stealth mode is enabled; a pass-through when it isn’t. The request is never touched.

It sits outside ErrorHandlingMiddleware on purpose: DispatchMiddleware is terminal and never calls the next handler, so only middleware ordered outside the error handler sees error and 404 responses — the ones most likely to carry a diagnostic header — as well as successful ones.

SettingDefaultEffect
core.stealth_modefalseMaster switch. When true, every response header whose name starts with X-Quiote- (matched case-insensitively) is removed, along with each name in core.stealth_additional_headers that is present.
core.stealth_additional_headers['X-Powered-By']Extra header names to strip, for headers that don’t carry the X-Quiote- prefix. Setting this replaces the default list, so include X-Powered-By yourself if you still want it gone.

That prefix rule covers the framework’s own diagnostics without listing them one by one — X-Quiote-Timing, X-Quiote-Trace, X-Quiote-Cache-Hit, X-Quiote-Validation-Errors and anything else emitted under the prefix.

Config/settings.php
'core.stealth_mode' => true,
'core.stealth_additional_headers' => ['X-Powered-By', 'X-App-Node'],

Stripping happens at the edge of the pipeline, so the headers are still set and still readable by everything downstream — turning stealth mode on doesn’t change what the rest of the stack does, only what leaves the process. Each strip is logged at debug level with the names removed.

A header a web server or proxy adds after PHP hands the response over — nginx’s or Apache’s own Server header, for instance — is outside this middleware’s reach; strip those in the server config.

Second in the stack (phase bootstrap, priority 1000), and the outermost middleware that handles a request rather than the response leaving. Catches any throwable from anything downstream and renders an error response. Runs by default — everything except StealthMiddleware sits inside it.

SettingDefaultEffect
core.developer_exceptionsfalseSelects the error renderer: true a detailed Whoops developer page, false a safe generic response that leaks no internals. This is the sole signal — there is no environment-name sniffing or separate debug env var.

Turn on the developer error page (never in production):

Config/settings.php
'core.developer_exceptions' => true,

The logger is wired as a constructor closure to Quiote\Logging\Log; there is no config switch for it short of replacing the pipeline.

Second in the stack (phase bootstrap, priority 950 — just inside ErrorHandlingMiddleware). When telemetry is enabled it opens the root request span and records the resource metrics (wall time, CPU, memory, cache-hit). When telemetry is off it’s a single pass-through, so it’s always safe to leave in the pipeline.

Its behaviour is driven entirely by the telemetry.* settings (and by whether the OpenTelemetry SDK is installed) — it has no settings of its own. The full settings table, sampling, spans, and log correlation are documented in Telemetry.

Quiote\Middleware\SessionMiddleware runs at bootstrap priority 900 — early, before security. It:

  • guarantees an ExecutionState request attribute exists;
  • loads or creates this request’s session from the incoming cookie and installs it on the context as the SessionBagInterface, so the User hierarchy, CSRF token storage, OIDC state and application code all reach the same session;
  • flushes request state — notably the authenticated user — on the way out, before the session is serialized;
  • persists the session and bakes the Set-Cookie onto the response.

Requests marked auth.sessionless or jwt.skip_session skip the session entirely and persist no user state, so a token-derived identity is never written into an unrelated session the client may still carry.

No settings of its own. Whether a session persists at all depends on the session role in your factories config. With no session role configured, the context keeps answering a NullSessionBag and this middleware does nothing beyond the ExecutionState guarantee. That is session configuration, not middleware configuration — see Sessions.

Distinct from Quiote\Session\SessionMiddleware, which is standalone PSR-15 wiring for an application driving SessionManager outside this pipeline. The pipeline one additionally owns the ExecutionState guarantee and the request-state flush.

Records total request time into ExecutionState metrics, optionally emitting an X-Quiote-Timing response header. A common one to disable in production — see Enabling and disabling.

SettingDefaultEffect
middleware.timing.emit_headerfalseWhether to emit the X-Quiote-Timing response header. Read straight from settings.* when the pipeline builds TimingMiddleware — no code, no re-registering.
Config/settings.php
'middleware.timing.emit_header' => true,

Appends each executed middleware’s name to ExecutionState metrics (a running trace), optionally emitting a trace response header. A common one to disable in production.

SettingDefaultEffect
middleware.trace.emit_headerfalseWhether to emit the trace response header.
middleware.trace.header_name'X-Quiote-Trace'Name of the emitted header.
Config/settings.php
'middleware.trace.emit_header' => true,
'middleware.trace.header_name' => 'X-Trace',

Both settings are read directly by MiddlewarePipeline when it builds the default stack — no MiddlewareCatalog::register() re-registration needed, unlike most other constructor-only behaviour on this page. If you do need to construct either middleware yourself (a custom pipeline, or a different position), the same values are just the emitHeader/headerName constructor arguments — see Writing custom middleware.

Unified request-body parser (JSON plus application/x-www-form-urlencoded), run before routing. Runs by default. Supersedes the older JsonBodyParsingMiddleware.

Env varDefaultEffect
QUIOTE_JSON_STRICTstrict on (unless set to 0)When strict and a JSON body is invalid, the request short-circuits with 400 {"error":"invalid_json"}. When non-strict (QUIOTE_JSON_STRICT=0), invalid JSON is ignored and the request proceeds unparsed.

This is an OS environment variable, not a Config key:

Terminal window
QUIOTE_JSON_STRICT=0 # tolerate malformed JSON bodies instead of 400-ing

Determines the negotiated output format (from the Accept header) before routing, storing output_type / output_formats request attributes. Runs by default. See Output types.

No exposed configuration. The fallback format (html) is a hardcoded class default.

Matches the request path, attaches module / action / output_type and an ActionDescriptor. Runs by default.

No settings of its own. It calls HttpMethodMapper, which reads routing.http_method_map — how you remap HTTP verbs to action methods is documented in Routing: Customising the HTTP verb mapping.

After routing resolves (or overrides) the output type, re-syncs the controller’s selection to match the output_type request attribute. Runs by default. No configuration.

Both CSRF middleware ship in the quioteframework/csrf package (added to the pipeline by Quiote\Security\Csrf\CsrfPlugin) — a required kernel dependency that’s registered automatically and on by default, so the entries below always apply unless you disable CSRF.

Wraps the response to inject CSRF tokens into HTML forms, a <meta> tag, and the readable XSRF-TOKEN cookie. Runs by default; its behaviour is gated at runtime by core.csrf.enabled.

Rejects unsafe-method requests with 403 unless a valid token is present. Runs by default; gated by core.csrf.enabled.

Two classes of request are exempted from the check automatically, with no per-route opt-out needed, because they fall outside CSRF’s threat model (an attacker riding a victim’s ambient session cookie):

  • A request an authenticator already resolved from a caller-supplied credential, signalled by the auth.stateless, auth.sessionless or jwt.skip_session request attributes. Note this is deliberately not “an Authorization header is present” — that header can be attached alongside a session cookie, so presence alone proved nothing.
  • A request with no session cookie and no foreign Origin. The Origin condition is what keeps a cross-site login POST — which also arrives without a session — from being exempted.

The session cookie name comes from the configured SessionManager (QSID by default), not from ext/session’s session_name(); ext/session is the fallback only when no session slot is configured. A route can force the check despite an exemption with a _csrf => true route default.

With no session slot at all there is nowhere to store a token, so same-origin unsafe requests are exempt (logged once per process as a warning) and cross-origin browser requests can never pass. See Sessions: the slot is optional and Authentication & authorization: automatic exemptions.

Both CSRF middleware read the same settings:

SettingDefaultEffect
core.csrf.enabledtrueMaster on/off for both injection and validation.
core.csrf.token_id'quiote_csrf'Token-id namespace for the Symfony token manager.
core.csrf.field_name'_csrf_token'Hidden form field injected into non-GET forms and read on submit.
core.csrf.header_name'X-CSRF-Token'Header that XHR/fetch/API clients send the token in.
core.csrf.cookie_name'XSRF-TOKEN'Readable (non-HttpOnly) cookie delivering the token to SPA clients.
core.csrf.safe_methods['GET','HEAD','OPTIONS','TRACE']Methods that skip validation (compared case-insensitively).
core.csrf.trusted_origins[]Origins that don’t count as foreign for the sessionless exemption. Compared host-only, so a TLS-terminating proxy’s scheme and port rewriting doesn’t reject same-site requests.
Config/settings.php
'core.csrf.enabled' => true,
'core.csrf.field_name' => '_csrf_token',
'core.csrf.header_name' => 'X-CSRF-Token',
'core.csrf.cookie_name' => 'XSRF-TOKEN',
'core.csrf.safe_methods' => ['GET', 'HEAD', 'OPTIONS', 'TRACE'],

The pipeline comment “gated at runtime by core.csrf.enabled” is documentation only — both middleware are always constructed and check CsrfManager::isEnabled() themselves. Per-route opt-out uses an _csrf => false route default. See Authentication & authorization: CSRF.

Decides, before dispatch, whether a request may run its action — forwarding to login or secure otherwise. Runs by default.

SettingDefaultEffect
core.use_securitytrueIf false, SecurityDecision::Allow is forced unconditionally — SecurityService::decide() is never consulted and all security checks are bypassed.

Per-action security is declared with isSecure() / getCredentials() — see Authentication & authorization.

StatelessAuthenticationMiddleware / SessionAuthenticationMiddleware

Section titled “StatelessAuthenticationMiddleware / SessionAuthenticationMiddleware”

Optional — contributed by the quioteframework/auth package’s AuthPlugin, not part of the default kernel stack. StatelessAuthenticationMiddleware (HTTP Basic/bearer authenticators) is anchored before: SessionMiddleware; SessionAuthenticationMiddleware (form login) is anchored after: RoutingMiddleware, before: SecurityMiddleware — see Where the auth packages insert themselves.

Both run the matched Firewall’s authenticator chain via AuthenticationManager and, on success, apply the resulting Passport to the request’s SecurityUser/RbacSecurityUser — before SecurityMiddleware ever checks isAuthenticated()/credentials.

No settings of their own. AuthPlugin registers an empty FirewallMap by default, so both middleware are a complete no-op — nothing to disable — until an app registers its own populated FirewallMap (by config via security.xml/SecurityConfigHandler, or by hand in a plugin). See Authenticating with the auth packages.

Runs the action’s validators and records a pass/fail decision; renders the error view (or an RFC 9457 problem document) with 400 on failure. Runs by default. See Validation.

SettingDefaultEffect
core.expose_validation_errors_headerfalseIf true, attaches a base64-encoded JSON of validator errors as X-Quiote-Validation-Errors on a 400. Off by default — it leaks internal field/validator structure; use only behind a trusted dev front end.
validation.reject_unknown_parameters'throw'Compile-time (not per-request) handling of a validator parameter not in the validator’s accepted list: 'throw' aborts the compile with a “did you mean” hint, 'warn' logs and records a diagnostic then continues, 'off' skips the check.

Note validation.reject_unknown_parameters lives under the validation. prefix, not core. — in XML that means a second <settings prefix="validation."> wrapper (see Configuration: The XML prefix attribute):

Config/settings.php
'core.expose_validation_errors_header' => false,
'validation.reject_unknown_parameters' => 'warn',

Establishes a slot stack so a view can render sub-actions (slots). Runs by default. No configuration. See Templates and rendering: Slots.

Runs the action through ActionExecutor, handles action/view caching, and builds the final PSR-7 response. Runs by default — effectively terminal.

SettingDefaultEffect
core.cache_enabledfalseMaster switch for action/view caching.
core.use_cachefalseWhether a cache instance is actually built to service the request.
core.disable-framework-headersfalseIf truthy, skips the framework response-header block entirely (both headers below).
core.cache-hit-header'X-Quiote-Cache-Hit'Header name sent (value 1) on a cache hit. Empty value suppresses it.
core.send-nosniff-headertrueWhether X-Content-Type-Options: nosniff is added when absent.

Caching requires both core.cache_enabled and core.use_cache to be true:

Config/settings.php
'core.cache_enabled' => true,
'core.use_cache' => true,

DispatchMiddleware also applies the resolved output type’s http_headers to the response. That is an output-type parameter (see Output types), not a core.* setting.

Post-processes the response to aggregate assets. Currently a pass-through placeholder. Runs by default. No configuration.

Re-fills submitted form values and error messages into HTML responses (so a failed submission doesn’t lose input). Runs by default.

No global settings. Its behaviour keys off Quiote\Util\FormPopulationConfig, a per-request state object (flags like force_request_uri), not a settings key.

Measures execution time and (for legacy-adapter responses) can append an <!-- exec_time=… --> comment. The HTML-comment behaviour is on by default in the class but the framework never disables it via config.


Any middleware can be switched off in the dedicated middleware.{xml,php,yaml,yml} config file, read by MiddlewareConfigHandler. A disabled middleware is never constructed. Each middleware’s #[Middleware(enabled:)] attribute sets its default (all framework middleware default to on); a <use> entry here overrides that default.

The file is a flat, ordered list of <use> entries. To disable a middleware, name its class and set enabled to false:

Config/middleware.php
return [
['class' => \Quiote\Middleware\ExecutionTimeMiddleware::class, 'enabled' => false],
['class' => \Quiote\Middleware\TimingMiddleware::class, 'enabled' => true],
];

Each <use> entry accepts these fields (only class is required; every other field left unset means “don’t override,” falling back to the class’s #[Middleware] attribute):

  • class — the exact middleware FQCN to target.
  • enabled — the on/off override. In XML, "0" / "false" / "off" / "no" (case-insensitive) count as disabled; anything else is enabled. In PHP/YAML it’s a plain boolean.
  • phase, priority, before, after — placement overrides (same meaning as the #[Middleware] attribute), so this file also registers app/plugin middleware and re-positions existing ones.
  • override-framework (XML) / override_framework (PHP/YAML) — must be true to change the placement of a framework middleware (a guard against accidentally reordering the core stack).

A class with no entry falls back to its attribute’s enabled default, so an entry is only needed to change the default. Disabling a core middleware (say CSRF or Security) removes real protection — do it deliberately.

Positioning your own middleware, and the whole-stack replacement API, are covered in Writing custom middleware:

  • A <use> entry in middleware.* — register or reposition middleware declaratively, no code (see Enabling and disabling above for the file’s shape).
  • The #[Middleware] attribute + MiddlewareCatalog::registerAttributed() — declare placement and join the framework’s ordering pass.
  • MiddlewareCatalog::register() — insert a middleware imperatively at a chosen point.
  • MiddlewareCatalog::replaceCoreStack() — replace the entire built-in stack (a deliberate footgun, gated behind an acknowledgement string).