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.
18 KiB
18 KiB
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.
Clientowns 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
Transportimplementations 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)
- Reuse connections via a long-lived client. Top-level one-shot request APIs
open a new connection per call; a
Clientinstance 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). - 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).
- 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).
- 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-Afterplus problem-detail bodies on errors (Microsoft API design best practices). - 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.jsonplus Swagger UI/ReDoc, and allows app-level overrides of the generated document (FastAPI OpenAPI docs). - 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,serversandsecuritytop-level objects, and path templating with{param}expressions (OpenAPI Specification v3.1.0). - Test through the app object. Starlette/FastAPI
TestClientwraps 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 verbsget/post/put/patch/delete/head/options,send(ClientRequest), sharedClientConfig, owns pool + jar + transport.client::Transport:send(request) -> responsecoroutine 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 hookextend(Document&).testing::TestClient:Clientsubclass fixed toAppTransport, lifespan helpers, fixture + clock wiring.
4. Rules (hard, numbered)
- All outbound HTTP from handlers, tasks and CLI goes through
client::Client; raw socket HTTP is forbidden outsidehttp_transport. - Every request has an explicit effective timeout (phases plus total); no request may wait unbounded.
- 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.
- Backoff is exponential with jitter and a static cap; max attempts is a static ceiling (default 3 total tries).
- Redirects are followed only for GET/HEAD by default, max 5 hops, and never forward Authorization across hosts.
- Cookie jar is per-Client, opt-in persistent, honors Secure/HttpOnly/ SameSite semantics on read-back.
- 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). - OpenAPI output is OpenAPI 3.1.x only; no 2.0/3.0 emitters.
- Generated OpenAPI is deterministic: stable key order, stable schema names, byte-identical output for identical app input.
- 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.
- 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_PROXYhonored, 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 optionalerrors[]for field failures (422 with field paths from validation). - Versioning: path prefix
/v{n}; breaking change requires new version. - Idempotency:
Idempotency-Keyheader supported on POST create endpoints; server stores keyed result 24h (see 04-data.md KV).
OpenAPI:
- Version string
3.1.0minimum; generator stampsx-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 throughAppTransport: no TCP, real everything else. Lifespan viatest_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 advanceclock.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)
- URL parser, header map, request/response value types with JSON/form/XML bodies (unit-covered via test client loopback only).
- Transport interface plus mock transport; Client verbs over injected transport; timeout phases enforced.
- Real HTTP transport on the 01-runtime.md loop: HTTP/1.1 first, then H2; pooling honoring caps; cookie jar; proxy incl. NO_PROXY.
- Retry policy with backoff/jitter/Retry-After; auth providers; redirect handling; streaming upload/download.
- REST helpers: Resource fluent API, paginator, problem-details errors, idempotency-key sender.
- OpenAPI document model + JSON/YAML serializers; schema converter from validation models; builder from routers; doc routes + embedded UI.
- Test client on AppTransport with lifespan; fixtures; frozen clock; assertion helpers; OpenAPI schema-conformance assertion.
- 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)
- URL/body round-trips: parse/serialize vectors incl. encoding edges; loopback test-client echo returns identical bodies.
- Mock-transport suite: scripted responses, predicate matching, timeout injection fires the right phase error, unconsumed-expectation failure.
- 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.
- 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.
- REST conformance: golden problem-details bodies; paginator walks a 250 item set; idempotent POST replay returns original result.
- 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.
- 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
- H2 for cleartext (h2c) in the client: prior-knowledge only, or upgrade handshake too? Proposal: prior-knowledge with opt-in upgrade.
- 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.
- 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
- OpenAPI Initiative - OpenAPI Specification v3.1.0 (2021-02-15): https://spec.openapis.org/oas/v3.1.0 (retrieved 2026-10-23).
- httpx documentation - Clients (Client pooling, config sharing): https://www.python-httpx.org/advanced/clients/ (retrieved 2026-10-23).
- httpx documentation - QuickStart (verbs, bodies, timeouts, auth): https://www.python-httpx.org/quickstart/ (retrieved 2026-10-23).
- httpx documentation - Transports (HTTP/ASGI/WSGI/Mock, retries, mounts): https://www.python-httpx.org/advanced/transports/ (retrieved 2026-10-23).
- FastAPI documentation - Testing (TestClient through app): https://fastapi.tiangolo.com/tutorial/testing/ (retrieved 2026-10-23).
- FastAPI documentation - Extending OpenAPI (code-first generation): https://fastapi.tiangolo.com/how-to/extending-openapi/ (retrieved 2026-10-23).
- 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).