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.
332 lines
18 KiB
Markdown
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).
|