Module: Tina4::DispatchPipeline
- Included in:
- RackApp
- Defined in:
- lib/tina4/dispatch_pipeline.rb
Overview
The dispatch pipeline: the concerns of RackApp#call, named and ordered.
#call was one 260-line function at cyclomatic complexity 53 against a ceiling of 10, on the path of every single request. Splitting it into a module is not only about that number: rack_app.rb also holds static file serving, the swagger UI, WebSocket upgrades and the error pages, so the pipeline was interleaved with four unrelated subsystems in one 999-line file. This is the pipeline, on its own, readable end to end.
Mixed into RackApp, so a stage can still call the app's helpers (#handle_route, #try_static, #dev_mode?) directly.
Defined Under Namespace
Classes: DispatchContext, RouteContext
Constant Summary collapse
- REQUEST_STAGES =
── The stages ────────────────────────────────────────
#call was one 260-line function at cyclomatic complexity 53 against a ceiling of 10, and it is on the path of every single request. The stages below are that function's concerns, named and ordered as DATA so the pipeline can be read, tested and compared across the four frameworks without reading an implementation.
Two phases, because the function genuinely has two:
REQUEST_STAGES run in order until one RETURNS a Rack triple. That triple is the response; later request stages do not run. `match_route` is terminal - it always produces one. ALWAYS_STAGES run over the triple no matter how it was produced - including the swagger and static branches, which return early and skip everything else. RESPONSE_STAGES run in order over the triple, each returning a new triple or nil to leave it unchanged. These are the post-processing steps (logging, injection, session save) that only apply to a dispatched response.Contract each stage obeys, asserted by spec/dispatch_pipeline_spec.rb:
* it takes (ctx) or (ctx, response) and nothing else - no stage reads a local of #call, because there are none left to read * it never calls another stage directly; ordering lives in these lists. `method_not_allowed` and `not_found` are therefore STAGES, not helpers called by `match_route` - the fallback chain is expressed as list order like everything else * a request stage returning nil means "not mine, keep going"Ordering is BEHAVIOUR here, not taste: dev routes must beat route matching, the pre-match middleware must run before the match so its headers survive a 401 (ADR-0012), and static resolution happens inside
match_route's not-found fallback because routes beat files (ADR-0010). %i[ reset_request_caches cors_preflight websocket_upgrade dev_routes feedback_routes global_middleware_pre match_route method_not_allowed not_found ].freeze
- ALWAYS_STAGES =
Runs on EVERY response, including the ones that bypass the rest.
RFC 9110 s9.3.2 is not conditional: a HEAD response MUST NOT carry content, whatever produced it. This used to live in RESPONSE_STAGES, so the swagger and static branches - which return early - skipped it, and
HEAD /style.cssshipped the whole file body. Measured 2026-07-31: Ruby returned 15 bytes where PHP, Python and Node all returned 0. %i[ head_strip apply_cors ].freeze
- RESPONSE_STAGES =
%i[ dev_inspector_capture request_log dev_toolbar_inject feedback_inject session_save ].freeze
- ROUTE_STAGES =
── The route pipeline ───────────────────────────────────────────
#handle_route was cyclomatic complexity 24 in one 118-line function, with the same two-phase shape as #call. These stages run in order until one returns a Rack triple (a 401/403 or a short-circuiting middleware); if none does, the handler is invoked and the result finalised.
Ordering is BEHAVIOUR, decided and written down (ADR-0012): the post-match global middleware runs BEFORE the auth gate so a rate limiter can throttle a brute-force login and an access log records the 401, while the route's OWN middleware runs AFTER it, so middleware attached to a secured route never processes an unauthenticated request.
%i[ prepare_route_request global_middleware_post route_auth_handler route_auth_gate route_middleware ].freeze