docs(arch): stream-client split + total-bytes cap for subscription forwards (FWD-15, FWD-14)
http-adapters.md: forwarding-handler step 5 documents the time/bytes split for Sub forwards (stream client without the total request timeout, total streamed-bytes cap, read timeout as the staleness guard); the HTTP Client section documents the two derived clients (request vs stream), the reqwest 0.13 per-request-override limitation that forces the derived-client design, and that only the total timeout differs between them. http-server.md: the per-endpoint dispatch deadline paragraph now covers the outbound half — unbounded time, bounded bytes on both the gateway and the forwarding side (ADR-049/021 note: unbounded time by design for subscriptions, bounded bytes per subscription). Verified: cargo doc --no-deps clean, full suite green.
This commit is contained in:
@@ -181,7 +181,7 @@ The forwarding handler is stored in the `HandlerRegistration` as a
|
||||
wraps it in a `ResponseEnvelope`, returns. Registered as
|
||||
`HandlerKind::Once` — a `Handler` returning a single
|
||||
`ResponseEnvelope`.
|
||||
5. For a `Sub` (`text/event-stream` response): streams
|
||||
5. For a `Sub` (`text/event-stream` response): streams
|
||||
`call.responded` events as the SSE chunks arrive (same SSE parsing as
|
||||
the TS `parseSSEFrames`), then the stream ends on SSE close (which
|
||||
becomes `call.completed` on the wire). Registered as
|
||||
@@ -189,6 +189,22 @@ The forwarding handler is stored in the `HandlerRegistration` as a
|
||||
`BoxStream<ResponseEnvelope>` (alkcall ADR-021). Each SSE `data:`
|
||||
frame becomes a `ResponseEnvelope::ok()`; an HTTP error (non-2xx)
|
||||
becomes a single `ResponseEnvelope::error()` and ends the stream.
|
||||
The streaming send rides the shared HTTP client's
|
||||
**stream client** (see HTTP Client below): the same config minus
|
||||
the total request timeout (FWD-15) — a subscription is unbounded in
|
||||
*time* by contract (alkcall ADR-021 sets `deadline: None`), so the
|
||||
outbound half must not die at the 30 s request deadline the
|
||||
request/response half carries. Time-unbounded does not mean
|
||||
memory-unbounded: the SSE parse loop enforces a **total
|
||||
streamed-bytes cap** per subscription
|
||||
(`HttpClientConfig.stream_total_byte_cap`, default 1 GiB),
|
||||
accumulated across every chunk fed to the parser (FWD-14);
|
||||
exceeding it terminates the stream with a single terminal error
|
||||
envelope (`HTTP_413`), the same stream-ends semantics as the other
|
||||
terminal arms. The bounded-time companion is the client's read
|
||||
timeout, which stays armed on the stream client: it bounds
|
||||
upstream *staleness* (no byte for 30 s → terminal), not stream
|
||||
lifetime.
|
||||
6. On HTTP error (non-2xx): maps to the declared `ErrorDefinition` by
|
||||
HTTP status code (see Error Fidelity below), returns a `CallError`.
|
||||
|
||||
@@ -205,6 +221,29 @@ connection pooling, keep-alive, TLS, and a retry stack. The shared type is
|
||||
both retry and Retry-After are middleware on the stack, and middleware
|
||||
requires the `ClientWithMiddleware` wrapper.
|
||||
|
||||
The shared `SharedHttpClient` exposes **two derived clients** from one
|
||||
config, rebuilt-and-swapped together (the same atomicity rule as the
|
||||
config itself):
|
||||
|
||||
- **Request client** (`SharedHttpClient::client`) — carries the total
|
||||
request timeout (default 30 s, anchored to the gateway's Once-op
|
||||
deadline). Every request/response forward (`from_openapi`/
|
||||
`from_jsonschema` Queries and Mutations) sends through it.
|
||||
- **Stream client** (`SharedHttpClient::stream_client`) — built from
|
||||
the same config with the total request timeout removed and the
|
||||
connect + read timeouts retained (FWD-15). Streaming (SSE)
|
||||
subscription forwards send through it. reqwest 0.13's per-request
|
||||
timeout override can lengthen a client-level total timeout but never
|
||||
clear it (the request-scoped `None` falls back to the client
|
||||
default), so a timeout-free subscription send requires the derived
|
||||
client rather than a request extension. Unbounded *time* per
|
||||
subscription is the contract (alkcall ADR-021); the complementary
|
||||
bounds are the read timeout (staleness) and the total streamed-bytes
|
||||
cap (see the forwarding-handler step 5 above).
|
||||
|
||||
Both clients carry the same middleware stack, redirect policy, and TLS
|
||||
trust — the total request timeout is the only delta between them.
|
||||
|
||||
The middleware stack has two layers:
|
||||
|
||||
1. **`RetryTransientMiddleware`** (from `reqwest-retry`) — exponential
|
||||
|
||||
@@ -385,7 +385,16 @@ registry invoke in `tokio::time::timeout`); a hung handler surfaces as
|
||||
a `TIMEOUT` error (`504`, `retryable: true`), not an indefinitely-held
|
||||
HTTP request. Streaming invokes (`/subscribe`) are unbounded —
|
||||
subscriptions are long-lived by contract (alkcall ADR-021 sets
|
||||
`deadline: None` for the streaming branch).
|
||||
`deadline: None` for the streaming branch). The same time/bytes split
|
||||
governs the **outbound** half of an imported subscription:
|
||||
`from_openapi`/`from_jsonschema` SSE forwards send through the shared
|
||||
HTTP client's stream client, built without the total request timeout
|
||||
(FWD-15) so a healthy subscription is not killed at the 30 s outbound
|
||||
deadline, while a total streamed-bytes cap
|
||||
(`HttpClientConfig::stream_total_byte_cap`, default 1 GiB) bounds the
|
||||
bytes a single subscription may pull (FWD-14) and the client's read
|
||||
timeout remains the upstream-staleness guard. Unbounded time, bounded
|
||||
bytes — on both the gateway and the forwarding side.
|
||||
|
||||
### `/healthz` (raw route)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user