Files
moloapp/plans/05-request-routing.md
T
retoor 54068f1953 docs: split plan into numbered detailed files with research
Ten sequential spec files plus index covering runtime,
observability, security, data, routing, realtime, supervisor,
clients, CLI ops, and workflow, each with best practices,
structure, rules, guidelines, specs, and cross-references.
2026-10-23 14:22:29 +02:00

303 lines
21 KiB
Markdown

# 05 - Request/Response, Routing, Views, Templates
Scope: plan.md sections 2, 6 partial, 10 step 5 partial.
Topics: app, routers, views, validation/forms, negotiation, templates, middleware, DI, errors.
## 1. Purpose and philosophy
This file owns everything between "bytes parsed by the runtime" and "value returned by user code": the
`Application` composition root, FastAPI-style routers, aiohttp-style class-based views, the single
validation framework (Pydantic-style models plus HTML/JSON forms), the response hierarchy with content
negotiation, the Jinja-like template engine, the middleware onion, dependency injection without globals,
and the typed error hierarchy.
Philosophy:
- One request object, one response hierarchy. Handlers never touch sockets; they take a `Request` and
return a `Response`. Streaming, SSE and realtime build on the same pair (see 06-realtime.md).
- Declarative over imperative. Routes, validation models, negotiation and DI bindings are declared;
the framework binds, validates, negotiates and injects before user code runs.
- A single validation framework feeds path, query, header, cookie and body binding, forms, and OpenAPI
schema generation (see 08-clients-openapi.md). No second ad-hoc parser anywhere.
- Templates are logic-light text renderers with secure defaults: auto-escape on for HTML/XML, explicit
`safe` opt-out only.
- Errors are typed values, not strings. Every framework exception maps to exactly one HTTP status and
renders as JSON for API clients or HTML for browsers, chosen by negotiation.
## 2. Best practices (from research)
- Routers group related endpoints and mount them on the app with a shared prefix, tags and dependencies.
FastAPI documents `APIRouter` plus `app.include_router(router)` as the scaling pattern for bigger
applications, where the include call applies prefix, tags, dependencies and responses to every route
in the router (Source: https://fastapi.tiangolo.com/tutorial/bigger-applications/, retrieved 2026-10-23).
- Class-based views derive from a base `View` holding the request and implement one coroutine per HTTP
method (`get`, `post`, ...). aiohttp documents `class MyView(web.View)` with `async def get(self)`
returning a response, registered on the router like a function handler
(Source: https://docs.aiohttp.org/en/stable/web_quickstart.html, retrieved 2026-10-23).
- Validation guarantees the output, not the input: untrusted data is parsed and coerced into a model
instance whose fields conform to declared types and constraints, else a single error listing all
failures is raised. Pydantic v2 states models this way: fields are annotated attributes on a
`BaseModel` subclass, and a `ValidationError` is raised when data cannot be parsed into an instance
(Source: https://pydantic.dev/docs/validation/latest/concepts/models/, retrieved 2026-10-23).
- Templates center on one reusable `Environment` holding loaders, filters, tests and globals; syntax is
`{{ ... }}` for expressions, `{% ... %}` for statements, `{# ... #}` for comments, with inheritance
(`extends`, `blocks`), includes, `for`/`if`, and autoescaping selectable per template name/extension
(Source: https://jinja.palletsprojects.com/en/stable/templates/, retrieved 2026-10-23).
- Content negotiation is server-driven: the client sends `Accept` with media ranges and optional `q`
weights, the server picks one proposal and answers with `Content-Type`, or `406 Not Acceptable`
when nothing matches (Source: https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Content_negotiation,
retrieved 2026-10-23; triangulated by RFC 9110, Section 12, which obsoletes RFC 7231,
Source: https://www.rfc-editor.org/rfc/rfc9110.html, retrieved 2026-10-23).
- Middleware forms an onion: each layer sees the request before the endpoint and the response after it,
and may short-circuit. Ordering rule derived from plan.md layering: exception mapping outermost,
then observability, security, session/auth, routing-adjacent concerns innermost.
## 3. Structure
Directories (one class or concern per file, deep over flat):
```
src/molo/app/ Application, sub-app mounting, lifespan, plugin registry
src/molo/routing/ Router, Route, path compiler, matchers, url_for
src/molo/http_msg/ Request, Response hierarchy, Headers, Query, Cookies view
src/molo/views/ View base, method dispatch, handler adapters
src/molo/validate/ Model, Field, validators, ValidationError, coercions
src/molo/forms/ Form, JSON binding, urlencoded/multipart binding
src/molo/negotiate/ Accept parser, q-sorting, format selection
src/molo/templates/ Environment, Loader, Lexer, Parser, Renderer, Filters, Tests
src/molo/middleware/ Middleware base, Chain, built-in middlewares owned here
src/molo/di/ Container, Scope, Provider, LuckilyNoGlobals
src/molo/errors/ HttpError hierarchy, handler, renderers (JSON, HTML)
```
Namespaces and key classes:
- `molo::app::Application`: owns routers, middleware chain, DI container, template env, error handler.
`include_router(prefix, router)`, `mount(path, sub_app)`, `on_startup/on_shutdown` hooks.
- `molo::routing::Router`: `get/post/put/patch/delete/head/options(path, handler)`; nested
`include_router`; `Route` record (methods, compiled path, handler, name, validation models, tags).
- `molo::http_msg::Request`: method, path, query, headers, cookies, `get<T>(key)` for query/form,
`json<T>()`, `form<T>()`, body access with caps from 01-runtime.md/03-security.md; `Response` base plus `HtmlResponse`,
`JsonResponse`, `XmlResponse`, `TextResponse`, `RedirectResponse`, `FileResponse`,
`StreamingResponse` (streaming mechanics live in 06-realtime.md, the class shell lives here).
- `molo::views::View`: constructed with `Request`; virtual `get/post/...` default to 405; `dispatch()`
selects by method. Function handlers adapt to the same interface.
- `molo::validate::Model`: field descriptors with type, constraints, default, required; `validate()`
returns model or throws `ValidationError` listing every field failure. Used for path/query/body.
- `molo::forms::Form`: binds `application/json`, `application/x-www-form-urlencoded`,
`multipart/form-data` (file parts delegate to upload handling in 03-security.md) into a `Model`.
- `molo::negotiate::Negotiator`: parses `Accept`, sorts by q, matches handler-declared formats.
- `molo::templates::Environment`: loaders, filters, tests, globals, autoescape predicate, cache.
- `molo::middleware::Middleware`: `handle(Request, Next) -> Response`; `Chain` composes in order.
- `molo::di::Container`: request-scoped and app-scoped providers, constructor injection for views.
- `molo::errors::HttpError(status, code, message, details)`; `ValidationHttpError` (422);
`ErrorHandler` maps exception to status plus negotiated JSON/HTML body.
## 4. Rules (hard, numbered)
- R1: Every route handler is a coroutine taking `Request` (or a `View` holding one) and returning a
`Response`. No handler touches sockets, the event loop, or globals.
- R2: All input binding (path, query, header, cookie, JSON body, form body) goes through
`molo::validate`. A second parser for the same input is a defect.
- R3: `ValidationError` lists ALL field failures in one response, never fail-fast on the first field.
- R4: Responses are chosen from the `Response` hierarchy only. Raw string returns from handlers are
rejected at compile time (no implicit conversion to 200 text).
- R5: Template autoescaping is ON for `.html`, `.htm`, `.xml` and for all streamed template output.
`safe` bypass requires naming the value safe at the call site; no global disable flag.
- R6: Content negotiation runs for every response with more than one declared format. Explicit
`?format=` or path suffix overrides `Accept` only when the route declares `allow_format_override`.
No match yields `406` with a JSON or HTML body listing available types.
- R7: Middleware order is fixed by category (errors outer, then observability, security, session/auth,
user middleware, router innermost). Reordering across categories is a compile-time or startup error.
- R8: DI provides DB, cache, auth, templates and settings. No service singletons, no file-static
mutable state reachable from handlers. Request scope ends at response completion.
- R9: Every framework exception maps to exactly one HTTP status via the error table in section 6.
Unmapped exceptions become 500 and are logged with correlation ID; stack details never leak to clients.
- R10: Route names are unique per application (including mounted sub-apps after path merge). Duplicate
names fail at startup with the colliding paths quoted.
- R11: All caps in section 6 are static constants with documented defaults. No autotuning, no silent
growth.
- R12: No feature in this file duplicates scope owned by a sibling: no TLS/auth/session logic (03-security.md),
no ORM/KV/cache (04-data.md), no WebSocket/SSE/Socket.IO mechanics (06-realtime.md), no OpenAPI emission (08-clients-openapi.md), no CLI
(09-cli-ops.md). This file exposes the hooks those siblings consume.
## 5. Development guidelines
- Build in the order of section 7; each step ends green with integration tests through real HTTP.
- Prefer small composable classes: one concern per file, behavior in exactly one base class, features
composed through declared interfaces (plan.md section 9).
- Headers follow the runtime: case-insensitive lookup, size and count caps enforced in 01-runtime.md before this
layer sees them.
- Keep the happy path allocation-light: route match without heap allocation per request where feasible;
template rendering reuses the compiled template cache.
- Every public API gets a usage example in the living example project (see 10-workflow-example.md)
in the same change that introduces it.
- Docs state current behavior with gaps listed next to the code that closes them; no aspirational APIs
in headers.
## 6. Specifications (concrete behaviors, caps, formats)
Route matching:
- Path syntax: `/users/{id:int}` with converters `int`, `float`, `str` (single segment), `path`
(remainder, must be last). Unknown converter is a startup error naming the route.
- Matching order: static segments first, then converters in registration order, then `path` catch-alls.
Method mismatch on an existing path yields 405 with an `Allow` header, not 404.
- Trailing slash: strict. `/users` and `/users/` are different routes unless the route sets
`slash_insensitive=true`, which registers both and canonical-redirects (308) to the declared form.
- Sub-app mount merges paths: mounting app B at `/app1` inside A prefixes all B routes with `/app1`;
triple nesting yields `/app1/app1/app1` automatically. Mounted apps keep their own DI scope but share
the parent middleware chain plus their own appended layers.
Validation and forms:
- Field constraints: `min/max` (numbers), `min_len/max_len` (strings/collections), `pattern` (regex),
`choices`, `gt/ge/lt/le`, `required`, `default`, `nullable`. Coercion: strict for path params
(no `"1abc"` to int), lenient-but-documented for query/form strings.
- `ValidationError` JSON shape: `{"detail":[{"loc":["query","age"],"msg":"...","type":"..."}]}`,
status 422. HTML rendering shows the same list in a 422 page.
- Form caps: max fields 1000, max field name 256 chars, max non-file value 1 MiB, max JSON depth 32,
max JSON document 10 MiB default (overridable per route down to 0 = reject bodies). File parts use
the upload quotas from 03-security.md.
Negotiation:
- Parse `Accept` into (range, q, params); sort by q desc, then specificity (exact beat type star beat
star star), then handler declaration order. Missing `Accept` means `*/*`.
- Handler declares `produces = {HTML, JSON}` etc. First negotiated match wins. No match: 406 with
`{"detail":{"available":["text/html","application/json"]}}` or the HTML equivalent.
- `Content-Type` of requests must match a route-declared `consumes` set when declared, else 415.
Templates (Jinja-compatible subset, in-house engine):
- Delimiters: `{{ }}`, `{% %}`, `{# #}`. Constructs: `extends`, `block`/`endblock`, `include`, `if`/
`elif`/`else`/`endif`, `for`/`else`/`endfor` with `loop` variable, `set`, `macro`, filters with `|`,
tests with `is`, `autoescape` blocks. Undefined variables render per policy: `chain` default renders
empty string and logs at debug; `strict` mode (dev default) throws `UndefinedError` (500 in prod
rendering, startup warning listing the template).
- Filters shipped: `escape`, `safe`, `upper`, `lower`, `trim`, `striptags`, `urlencode`, `length`,
`default`, `join`, `date`, `tojson`. Tests shipped: `defined`, `none`, `string`, `number`,
`sequence`, `mapping`, `even`, `odd`.
- Caps: max template size 1 MiB, max include depth 16, max extends chain 8, max loop iterations 100000,
max render output 32 MiB, compiled-template cache max 512 entries with LRU eviction. Recursion beyond
depth aborts with 500 and a metric increment, never a stack overflow.
- Loaders: filesystem directory, embedded assets (from 09-cli-ops.md embedder), in-memory dict. Dev reloads from
disk per render when `dev_reload=true`; prod serves compiled cache only.
Middleware and DI:
- Signature: `Task<Response> handle(Request req, Next next)`. `Next` invokes the remainder exactly once;
calling twice is a runtime error. Middleware may replace request/response but must preserve the
correlation ID (owned by 02-observability.md).
- Built-ins owned here: `ExceptionMappingMiddleware` (outermost), `NegotiationMiddleware`,
`CORSPassthrough` is NOT here (03-security.md owns CORS policy; this file only preserves its headers),
`RequestIdEcho` helpers defer to 02-observability.md. User middleware registers with an explicit category tag.
- DI scopes: `app` (one per Application), `request` (one per request, destroyed after response).
Providers are factories or values; circular dependencies fail at startup with the cycle quoted.
Errors:
- Mapping table: 400 `BadRequest`, 401 `Unauthorized`, 403 `Forbidden`, 404 `NotFound` (includes
`url_for` miss at render time), 405 `MethodNotAllowed` (sets `Allow`), 406 `NotAcceptable`,
408/413/414/431 from runtime caps pass through, 415 `UnsupportedMediaType`, 422 `ValidationFailed`,
429 from 03-security.md passes through, 500 `InternalError`, 503 `Unavailable`.
- JSON error shape: `{"error":{"code":"not_found","message":"...","request_id":"..."}}`.
HTML error shape: responsive error page with the same fields, no internals.
- Redirects: `RedirectResponse(url, permanent)` emits 308/307 (method-preserving). Flash messages ride
the session (03-security.md) via `flash(request, level, message)` and render once.
Global caps (static, overridable by config, never autotuned):
- max routes per app 100000; max path length 2048 chars; max path params per route 32;
max middleware layers 64; max sub-app nesting depth 8; max DI providers 4096.
## 7. Execution steps (in build order)
- E1: `Request`, `Headers`, `Query`, cookie view, `Response` base plus Text/JSON/HTML/XML/Redirect.
No routing yet; handlers invoked directly in tests.
- E2: Path compiler plus `Router` (static, converters, 404/405, Allow header), `url_for` by name.
- E3: `Application`, `include_router` with prefix/tags/dependencies, sub-app `mount` with path merge,
startup validation (duplicate names, unknown converters, cycles).
- E4: `molo::validate` (Model, Field, coercions, full-error `ValidationError`) bound to path, query,
headers, cookies.
- E5: `molo::errors` (hierarchy, mapping table, JSON plus HTML renderers, global handler).
- E6: `molo::negotiate` plus `NegotiationMiddleware`, `consumes/produces` declarations, 406/415 paths.
- E7: `molo::views::View` with method dispatch, function-handler adapter, redirects, flash helpers.
- E8: Middleware `Chain` with category ordering enforcement and `Next`-once guard.
- E9: `molo::di` (app and request scopes, cycle detection) wired into views and handlers.
- E10: `molo::forms` (JSON, urlencoded, multipart field parts) on top of validation.
- E11: Template engine: lexer, parser, renderer, Environment, loaders, filters/tests, autoescape,
caps and compiled cache. `TemplateResponse(template, context)` as a `Response` subclass.
- E12: Plugin/extension registry on `Application` (named hooks: route_added, app_started, error_render)
with capability declarations so plugins cannot reach undeclared internals.
## 8. Validation (how to verify each step)
Integration tests only, through the real stack (real HTTP, routing, middleware), per plan.md section 8.
Test files mirror routes: `tests/routes/<router>/<route>_test.cpp`.
- V1: GET/POST round-trips return correct status, headers and bodies for each response subclass.
- V2: static, converter and catch-all matches; 404 body shape; 405 with exact `Allow` value;
`url_for` reverses every named route including converted params.
- V3: prefixed includes; triple-nested mount produces `/app1/app1/app1`; duplicate names and unknown
converters fail startup with quoted diagnostics.
- V4: valid input binds; invalid input yields one 422 listing every bad field (path, query, header,
cookie cases each covered).
- V5: each mapped exception renders the exact JSON shape and the HTML page; unknown exception yields
500 JSON/HTML without internals; `request_id` present in both.
- V6: q-weighted `Accept` fixtures pick the expected format; no-match yields 406 listing available
types; wrong `Content-Type` yields 415.
- V7: View dispatches each method; unimplemented method yields 405; redirects preserve method (307/308);
flash message appears exactly once across two requests.
- V8: middleware order test asserts onion sequence; double-`Next` call fails loudly; short-circuit
returns without reaching the handler.
- V9: request scope isolation across two concurrent requests; app scope shared; cycle fixture fails
startup quoting the cycle.
- V10: JSON, urlencoded and multipart forms bind to the same model; cap fixtures (1001 fields, depth
33, oversized value) are rejected with the documented status.
- V11: template fixtures cover extends/blocks, include, if/for/else, filters, tests, macros; XSS
fixture asserts escaping; `safe` fixture asserts opt-out; cap fixtures (depth 17, 100001 iterations)
abort safely; dev reload picks up an edited file, prod serves cache.
- V12: sample plugin receives hooks in order and is denied undeclared access.
## 9. References (sibling files this depends on or feeds)
- Depends on 01-runtime.md: timeouts, body/header caps, cookies, compression, static serving. This
file never re-enforces byte-level caps.
- Depends on 02-observability.md: correlation IDs, log format, per-route metrics, tracing spans.
- Depends on 03-security.md: CSRF, CORS policy, rate limits, session/flash storage, upload quotas,
auth guards consumed by views and DI.
- Depends on 04-data.md: ORM/KV/cache handles provided through DI; explicit invalidation hooks.
- Feeds 06-realtime.md: `Request`/`Response` shells, router mounting and middleware/DI participation
for WebSocket/SSE/Socket.IO views and streaming responses.
- Feeds 07-supervisor-replication.md: request scope teardown points safe for background handoff.
- Feeds 08-clients-openapi.md: routers plus validation models as the OpenAPI source; response formats
as client expectations; test client drives `Application` without network.
- Feeds 09-cli-ops.md: dev server hosts `Application`; embedder output plugs into template loaders;
health endpoints are plain routes defined here.
- Follows 10-workflow-example.md: specs, test layout, living example coverage for every API above.
## 10. Open questions
- Q1: Exact lenient coercion table for query/form strings (e.g. `"yes"` to bool, empty string to
null). Proposal: strict booleans (`true/false/1/0` only), empty string never coerces to null.
- Q2: Whether `?format=` override defaults on or off. Proposal: off; routes opt in.
- Q3: Template `strict` undefined policy in production: render empty plus warn, or 500. Proposal:
empty plus warn and metric, 500 only in dev/test.
- Q4: Plugin capability model granularity: coarse hook names now, fine-grained review before v1.
## 11. Sources (exact URLs with retrieval dates)
- FastAPI tutorial, Bigger Applications (APIRouter, include_router):
https://fastapi.tiangolo.com/tutorial/bigger-applications/ (retrieved 2026-10-23)
- aiohttp Web Server Quickstart (Application, handlers, class-based views):
https://docs.aiohttp.org/en/stable/web_quickstart.html (retrieved 2026-10-23)
- Pydantic v2 concepts, Models (validation guarantees, ValidationError):
https://pydantic.dev/docs/validation/latest/concepts/models/ (retrieved 2026-10-23)
- Jinja Template Designer Documentation (syntax, inheritance, autoescape):
https://jinja.palletsprojects.com/en/stable/templates/ (retrieved 2026-10-23)
- MDN, Content negotiation (Accept, q weights, 406):
https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Content_negotiation (retrieved 2026-10-23)
- RFC 9110, HTTP Semantics, Section 12 (content negotiation, obsoletes RFC 7231):
https://www.rfc-editor.org/rfc/rfc9110.html (retrieved 2026-10-23)