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

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. 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).