Files
moloapp/plans/08-clients-openapi.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

332 lines
18 KiB
Markdown

# 08 - Clients, OpenAPI, Test Client
Title: Clients, OpenAPI, Test Client
Scope: plan.md sections 2, 6, 8 partial, 10 step 5 partial.
Topics: HTTP/REST client, REST standards, OpenAPI generation, test client.
## 1. Purpose and philosophy
This file plans the outbound and introspection side of MoloApp: the modern
async HTTP/API/REST client, the REST conventions every app follows, automatic
OpenAPI document generation from routers plus validation models, and the test
client that drives the real stack without real network I/O.
Philosophy:
- One client core. `Client` owns pooling, timeouts, retries, auth, cookies,
proxy, TLS, logging. Convenience free functions exist only for one-off
scripts and never pool.
- Client parity. Everything the framework serves ships a client. This file
owns HTTP/REST; realtime clients live in 06-realtime.md and reuse the
timeout, retry, auth, logging and backoff behavior defined here.
- Transport injection. Real network, in-process app dispatch, and scripted
mocks are all `Transport` implementations behind one interface. Tests swap
the transport, never the calling code.
- Spec from code. Routers plus validation models are the single source of
truth; the OpenAPI document is derived, never hand-maintained.
- Tests through the real stack. The test client runs routing, middleware,
validation, views and DB for real; only the socket is fake.
## 2. Best practices (from research)
1. Reuse connections via a long-lived client. Top-level one-shot request APIs
open a new connection per call; a `Client` instance pools and reuses TCP
connections to the same host, cutting latency, CPU and congestion. Client
instances additionally carry cookie persistence, shared config, proxying
and HTTP/2 support (httpx Clients doc).
2. Separate timeout phases. Configure connect, read, write and pool timeouts
independently with a sane default total, so slow servers and slow networks
fail fast in the right place (httpx Timeouts/Clients docs).
3. Retry only what is safe. Retry connect errors and idempotent methods on
429/502/503/504 with exponential backoff and jitter; never auto-retry
non-idempotent bodies unless explicitly enabled (httpx Transports doc on
connection retries; Microsoft REST guidance on throttling with Retry-After).
4. Design resources, not RPC. Nouns in URIs, verbs in HTTP methods, plural
collection names, stateless requests, standard status codes, versioning by
path or media type, pagination/filtering/sorting as query parameters, and
`Retry-After` plus problem-detail bodies on errors (Microsoft API design
best practices).
5. Generate OpenAPI from code annotations. FastAPI derives paths, parameters,
request/response schemas (JSON Schema), security schemes and examples from
route decorators plus Pydantic models, serves `openapi.json` plus Swagger
UI/ReDoc, and allows app-level overrides of the generated document
(FastAPI OpenAPI docs).
6. Emit OpenAPI 3.1. The 3.1.x line aligns schemas fully with JSON Schema
(draft 2020-12), uses `openapi: 3.1.x`, `info`, `paths`, `components`,
`tags`, `servers` and `security` top-level objects, and path templating
with `{param}` expressions (OpenAPI Specification v3.1.0).
7. Test through the app object. Starlette/FastAPI `TestClient` wraps an HTTP
client whose transport calls the ASGI app in-process: same routing,
middleware, validation and responses as production, no socket. Lifespan
events run via context-manager entry (FastAPI Testing tutorial; httpx
ASGI/WSGI/Mock transports doc).
## 3. Structure
Directories (one class or concern per file, deep over flat):
```
src/client/
client.h, client.cpp # molo::client::Client, config + verbs
request.h, request.cpp # ClientRequest builder (method, URL, headers, body)
response.h, response.cpp # ClientResponse (status, headers, body, json/xml/form)
url.h, url.cpp # URL parse, join, query encode
headers.h # shared header map helpers (thin, delegates to runtime)
cookies.h, cookies.cpp # CookieJar, persistence across requests
auth.h, auth.cpp # Basic, Bearer, ApiKey, custom Auth base
timeouts.h # Timeout phases struct
retry.h, retry.cpp # RetryPolicy, backoff with jitter
pool.h, pool.cpp # connection pool (delegates sockets to 01-runtime.md)
proxy.h, proxy.cpp # proxy selection incl. NO_PROXY matching
transport.h # Transport interface
transports/
http_transport.h, .cpp # real network via runtime event loop
mock_transport.h, .cpp # scripted request->response double
app_transport.h, .cpp # in-process dispatch into Application (test client base)
body/
json_body.h, .cpp
form_body.h, .cpp # urlencoded + multipart, file upload parts
xml_body.h, .cpp
stream_body.h, .cpp # chunked upload/download with backpressure
rest/
resource.h, .cpp # Resource fluent helper (CRUD over a base path)
paginator.h, .cpp # page/limit/offset/cursor iteration
errors.h, .cpp # raise-for-status, ProblemDetails parse
src/openapi/
document.h, .cpp # OpenAPI document model, serialize JSON (+YAML)
builder.h, .cpp # builds document from routers + validation models
schema.h, .cpp # validation-model to JSON Schema converter
routes.h # serves /openapi.json, /docs (Swagger UI), /redoc
extensions.h # x- vendor extensions, app overrides hook
src/testing/
test_client.h, .cpp # TestClient: Client preset with AppTransport
fixtures.h, .cpp # DB/ORM fixtures, seeded app factory
time.h, .cpp # frozen/manual clock injected into runtime + ORM
assertions.h, .cpp # status/json/header/schema assertion helpers
```
Namespaces: `molo::client`, `molo::client::transports`, `molo::client::rest`,
`molo::openapi`, `molo::testing`.
Key classes:
- `client::Client`: coroutine verbs `get/post/put/patch/delete/head/options`,
`send(ClientRequest)`, shared `ClientConfig`, owns pool + jar + transport.
- `client::Transport`: `send(request) -> response` coroutine interface.
- `client::RetryPolicy`: max attempts, backoff base/cap, jitter, retryable
status set, idempotency gate.
- `rest::Resource`: `collection("users").get(id)`, `.list(query)`,
`.create(doc)`, `.update(id, doc)`, `.remove(id)`.
- `openapi::Builder`: `from_app(const App&) -> Document`, deterministic
output, app override hook `extend(Document&)`.
- `testing::TestClient`: `Client` subclass fixed to `AppTransport`,
lifespan helpers, fixture + clock wiring.
## 4. Rules (hard, numbered)
1. All outbound HTTP from handlers, tasks and CLI goes through
`client::Client`; raw socket HTTP is forbidden outside `http_transport`.
2. Every request has an explicit effective timeout (phases plus total); no
request may wait unbounded.
3. Retries apply only to idempotent methods (GET, HEAD, OPTIONS, PUT, DELETE)
plus POST explicitly marked idempotent (Idempotency-Key header or policy
flag); anything else retries only connect-phase failures, never a sent
body.
4. Backoff is exponential with jitter and a static cap; max attempts is a
static ceiling (default 3 total tries).
5. Redirects are followed only for GET/HEAD by default, max 5 hops, and never
forward Authorization across hosts.
6. Cookie jar is per-Client, opt-in persistent, honors Secure/HttpOnly/
SameSite semantics on read-back.
7. REST surface rules: plural noun collections, no verbs in paths, correct
method semantics (POST create, GET read, PUT full replace, PATCH partial,
DELETE remove), standard status codes (200/201/204/400/401/403/404/409/
422/429/500), errors as RFC 9457 problem details (`application/problem+json`).
8. OpenAPI output is OpenAPI 3.1.x only; no 2.0/3.0 emitters.
9. Generated OpenAPI is deterministic: stable key order, stable schema names,
byte-identical output for identical app input.
10. Test client runs the real stack (routing, middleware, auth, validation,
views, DB). Tests that bypass routing to call views directly are
forbidden except for documented error-path probes.
11. All caps in section 6 are static constants, never autotuned.
## 5. Development guidelines
- Build order inside this file: URL + headers + bodies, then Transport +
Client + pool + timeouts, then retry/auth/cookies/proxy, then REST helpers,
then OpenAPI model + builder + schema converter + doc routes, then test
client + fixtures + clock.
- Reuse, do not reimplement: sockets, TLS, HTTP/1.1+H2 framing and
compression come from 01-runtime.md; auth/session semantics from
03-security.md; validation models from 05-request-routing.md; logging,
metrics, traces from 02-observability.md.
- Every public client API has sync (blocking on internal loop bridge for CLI
use) and coroutine forms sharing one implementation; the coroutine form is
primary.
- Streaming is first-class: uploads and downloads support bounded buffers
with backpressure; files over 1 MB must be able to stream without full
buffering.
- Mock transport matches on method + path template + headers/body predicates
and asserts unconsumed expectations at teardown.
- OpenAPI builder reads route metadata (method, path template, summary,
tags, auth guards, validation models for params/body/response, status
codes). Anything not declared in code appears as `description: TBD`, never
as invented detail.
- Serve Swagger UI and ReDoc from embedded assets (see 09-cli-ops.md
embedder); docs routes are disabled by default in production config.
- Test client API mirrors the real client verb-for-verb so tests read like
production call sites.
## 6. Specifications (concrete behaviors, caps, formats)
Client:
- Protocols: HTTP/1.1 and HTTP/2 (via 01-runtime.md). H2 enabled per-client
flag, default on for https URLs.
- Timeout defaults: connect 5s, read 30s, write 30s, pool acquire 5s, total
60s. All overridable per request.
- Pool caps: max 100 connections per Client, max 20 per host, max keepalive
60s, max idle 10 per host. Queue wait bounded by pool-acquire timeout.
- Retry defaults: 3 total attempts, base 200ms, cap 5s, full jitter,
retryable statuses {429, 502, 503, 504}, honor `Retry-After` (seconds and
HTTP-date) up to cap.
- Redirects: max 5, GET/HEAD auto, 307/308 preserve method+body, strip
Authorization on cross-host.
- Body caps: in-memory body max 10 MB default; larger requires streaming
API. Multipart file part max 512 MB, total request max 1 GB (static caps,
lowered by server policy in 03-security.md).
- Content helpers: `json<T>()` (serialize/parse via validation framework),
`form()`, `multipart()`, `xml()`, `bytes()`, `stream()`.
- Proxy: `HTTP_PROXY/HTTPS_PROXY/NO_PROXY` honored, per-client override,
wildcard + suffix + CIDR NO_PROXY match.
- Observability: every request emits span + metrics (duration, status,
retries, bytes) and debug log with correlation ID; disabled paths compile
near-zero per 02-observability.md.
REST standards:
- URI style: `/v1/users/{id}`, lowercase, hyphenated, plural collections,
query for filter/sort/page (`?page=&per_page=&sort=&filter=`).
- Pagination: default page+per_page (max per_page 100, default 20); cursor
alternative for large sets. Envelope: `{data, meta:{page,per_page,total}, links}`.
- Errors: RFC 9457 `{type,title,status,detail,instance}` plus optional
`errors[]` for field failures (422 with field paths from validation).
- Versioning: path prefix `/v{n}`; breaking change requires new version.
- Idempotency: `Idempotency-Key` header supported on POST create endpoints;
server stores keyed result 24h (see 04-data.md KV).
OpenAPI:
- Version string `3.1.0` minimum; generator stamps `x-molo-framework-version`.
- Served at `/openapi.json` (JSON, canonical) and `/openapi.yaml`; UI at
`/docs` (Swagger) and `/redoc`.
- Schemas: every validation model becomes `#/components/schemas/<Name>`;
path/query/header/cookie params from binding declarations; security
schemes from auth guards (http/bearer, http/basic, apiKey, oauth2 as
configured); examples from model examples.
- Generation cap: max 5000 paths, max 2000 schemas, document max 10 MB;
beyond that the builder errors with a clear message.
Test client:
- `TestClient(App)` issues requests through `AppTransport`: no TCP, real
everything else. Lifespan via `test_client.lifespan()` guard.
- Fixtures: per-test DB reset (SQLite in-memory or isolated Postgres
schema), seeded objects factory, ORM session bound to request scope.
- Clock: frozen time default in tests (`testing::Clock::freeze(t)`), manual
advance `clock.advance(d)`; runtime timers and ORM timestamps read it.
- Assertions: `expect_status`, `expect_json`, `expect_json_schema(name)`
validated against the generated OpenAPI schemas, `expect_header`.
## 7. Execution steps (in build order)
1. URL parser, header map, request/response value types with JSON/form/XML
bodies (unit-covered via test client loopback only).
2. Transport interface plus mock transport; Client verbs over injected
transport; timeout phases enforced.
3. Real HTTP transport on the 01-runtime.md loop: HTTP/1.1 first, then H2;
pooling honoring caps; cookie jar; proxy incl. NO_PROXY.
4. Retry policy with backoff/jitter/Retry-After; auth providers; redirect
handling; streaming upload/download.
5. REST helpers: Resource fluent API, paginator, problem-details errors,
idempotency-key sender.
6. OpenAPI document model + JSON/YAML serializers; schema converter from
validation models; builder from routers; doc routes + embedded UI.
7. Test client on AppTransport with lifespan; fixtures; frozen clock;
assertion helpers; OpenAPI schema-conformance assertion.
8. Living-example coverage: REST client flows, OpenAPI endpoints, and one
full test-client suite per 10-workflow-example.md.
## 8. Validation (how to verify each step)
1. URL/body round-trips: parse/serialize vectors incl. encoding edges;
loopback test-client echo returns identical bodies.
2. Mock-transport suite: scripted responses, predicate matching, timeout
injection fires the right phase error, unconsumed-expectation failure.
3. Real-transport suite against local test server: keep-alive reuse proven
by connection counter, pool caps enforced under burst, H2 negotiated on
TLS fixture, proxy + NO_PROXY routing matrix.
4. Retry matrix: each retryable status retried then succeeding; POST without
idempotency key not retried after body sent; Retry-After honored; redirect
Authorization-strip test; 50 MB streamed download under 16 MB RSS growth.
5. REST conformance: golden problem-details bodies; paginator walks a 250
item set; idempotent POST replay returns original result.
6. OpenAPI: generated document validates against the official 3.1 JSON
schema; byte-determinism check (generate twice, diff empty); snapshot of
example app reviewed; UI routes return 200 in dev, 404 in prod default.
7. Test client: full suite runs with loopback transport only (assert zero
real sockets via pool counter); lifespan startup/shutdown observed;
frozen-clock test proves identical timestamps; schema-conformance
assertion passes for every example route.
## 9. References
Depends on:
- 01-runtime.md: event loop, HTTP/1.1+H2 framing, TLS hooks, compression,
connection primitives the pool and HTTP transport build on.
- 02-observability.md: spans, metrics, log format, correlation IDs, zero-cost
macros used by every request.
- 03-security.md: auth/session semantics, TLS via wolfSSL, body/upload caps,
redirect Authorization rules.
- 04-data.md: KV for idempotency keys, DB fixtures base for test client.
- 05-request-routing.md: routers, validation models, response hierarchy,
error mapping; OpenAPI builder inputs.
Feeds / defers to:
- 06-realtime.md: owns WebSocket/SSE/Socket.IO/WebDAV clients; reuses this
file's timeout/retry/auth/logging/backoff contracts.
- 09-cli-ops.md: AI harness CLI is built on this HTTP client; embedder packs
Swagger/ReDoc UI assets; dev server hosts doc routes.
- 10-workflow-example.md: owns test-file layout/naming and living-example
gates; this file provides the TestClient mechanism it standardizes on.
## 10. Open questions
1. H2 for cleartext (h2c) in the client: prior-knowledge only, or upgrade
handshake too? Proposal: prior-knowledge with opt-in upgrade.
2. Should the OpenAPI builder also emit AsyncAPI fragments for WS/SSE
routes, or stay HTTP-only and let 06-realtime.md define its own export?
Proposal: HTTP-only here, cross-link.
3. Idempotency-key storage backend default: in-memory with documented loss
on restart, or KV-backed always? Proposal: KV-backed, in-memory only for
single-process dev.
## 11. Sources
1. OpenAPI Initiative - OpenAPI Specification v3.1.0 (2021-02-15):
https://spec.openapis.org/oas/v3.1.0 (retrieved 2026-10-23).
2. httpx documentation - Clients (Client pooling, config sharing):
https://www.python-httpx.org/advanced/clients/ (retrieved 2026-10-23).
3. httpx documentation - QuickStart (verbs, bodies, timeouts, auth):
https://www.python-httpx.org/quickstart/ (retrieved 2026-10-23).
4. httpx documentation - Transports (HTTP/ASGI/WSGI/Mock, retries, mounts):
https://www.python-httpx.org/advanced/transports/ (retrieved 2026-10-23).
5. FastAPI documentation - Testing (TestClient through app):
https://fastapi.tiangolo.com/tutorial/testing/ (retrieved 2026-10-23).
6. FastAPI documentation - Extending OpenAPI (code-first generation):
https://fastapi.tiangolo.com/how-to/extending-openapi/ (retrieved 2026-10-23).
7. Microsoft Learn - Web API design best practices (REST guidance):
https://learn.microsoft.com/en-us/azure/architecture/best-practices/api-design (retrieved 2026-10-23).