fix(gateway): SSE terminality, deadline, error-mapping fidelity (GW-03..GW-07, GW-12..GW-14)

- GW-04: /subscribe error events are terminal — scan-based emission of
  the error frame, then end of stream (matches call.error semantics).
- GW-05: enforce the 30 s gateway deadline via tokio::time::timeout in
  GatewayDispatch::invoke; hung handlers surface as TIMEOUT (504),
  streaming/sink stay unbounded per ADR-021.
- GW-07: gateway error paths route through the new identity-aware
  call_error_to_http_response_with_identity; retryable HTTP_429/HTTP_503
  now carry Retry-After on /call, /batch, /search, /schema, /publish.
- GW-13: SSE keep-alive (15 s comment frames) + retry: 15000 field.
- GW-14: module doc fixed (6 endpoints; /publish lives in routes.rs).
- GW-03/GW-12: mapping rides d7ee302's INVALID_OPERATION_TYPE mapper
  (documented in http-server.md table); 200-on-stream asymmetry
  documented.

Verification: cargo test (243 passed); cargo clippy --all-targets -- -D
warnings clean; cargo fmt --check clean.
This commit is contained in:
2026-08-29 10:13:03 +00:00
parent 713f1eba46
commit 9bc9e669e1
5 changed files with 422 additions and 45 deletions
@@ -1,7 +1,7 @@
---
id: review-001-gateway-stream-errors
name: Gateway error-fidelity — SSE terminality, mappings, Retry-After, deadline (GW-03..GW-07, GW-12..GW-14)
status: pending
status: completed
depends_on: []
scope: narrow
risk: low
@@ -51,12 +51,12 @@ transports:
## Acceptance Criteria
- [ ] SSE stream ends after an error event (test); keep-alive present
- [ ] `INVALID_OPERATION_TYPE` maps to the same status on `/call`, `/batch`, `/publish`; http-server.md table updated
- [ ] Once-op invokes enforce the 30 s deadline (test with a hung handler)
- [ ] Retryable errors carry `Retry-After` on all live error paths (test)
- [ ] GW-14 docs fixed (module doc; AGENTS.md gateway-endpoint count if touched)
- [ ] `cargo test` and `cargo clippy --all-targets -- -D warnings` pass
- [x] SSE stream ends after an error event (test); keep-alive present
- [x] `INVALID_OPERATION_TYPE` maps to the same status on `/call`, `/batch`, `/publish`; http-server.md table updated
- [x] Once-op invokes enforce the 30 s deadline (test with a hung handler)
- [x] Retryable errors carry `Retry-After` on all live error paths (test)
- [x] GW-14 docs fixed (module doc; AGENTS.md gateway-endpoint count if touched)
- [x] `cargo test` and `cargo clippy --all-targets -- -D warnings` pass
## References
@@ -70,6 +70,57 @@ transports:
> review-001-gateway-publish-semantics (both touch routes.rs —
> sequence or coordinate to avoid churn).
Built on d7ee302 (GW-01/GW-06/HY-13 publish semantics + the error.rs
`INVALID_OPERATION_TYPE` mapper) — not re-implemented or reverted.
GW-03 needed no new mapping: the publish-side mapper from d7ee302
(`422` with identity / `401` without) already covers `/call` and
`/batch` because both route their error envelopes through the shared
mapper; documented in http-server.md's error table.
## Summary
> Filled on completion.
- **GW-04**: `subscribe_stream_from_envelope_stream` reframed with a
`scan`-based projection — the first `Err` envelope's `event:error`
frame is emitted, then the stream terminates (the scan flags done and
subsequent polls return `None`). New test
`subscribe_stream_is_terminal_after_an_error_event` proves a
post-error envelope never reaches the wire. Matches the wire
dispatcher's `call.error`-is-terminal semantics.
- **GW-03**: no code change needed beyond d7ee302's mapper — verified
consistent `422`/`401` across `/call`/`/batch`/`/publish` via the
existing `error_streaming_handler`-style tests plus the mapper's unit
tests; http-server.md error table now documents
`INVALID_OPERATION_TYPE` explicitly (422 with token / 401 without).
- **GW-05**: enforced, not removed. `GatewayDispatch::invoke` wraps the
registry invoke in `tokio::time::timeout(DEFAULT_TIMEOUT)`; a hung
handler yields a `TIMEOUT` error envelope (`504`,
`retryable: true`) instead of holding the request open. Streaming and
sink invokes stay unbounded (ADR-021 sets `deadline: None` for
subscriptions; a `/publish` body is upload-bounded). Tests: hung
handler (120 s sleep) returns well under 60 s with `TIMEOUT`; fast
handler unaffected. The `deadline:` `OperationContext` metadata stays
(it feeds registry-side bookkeeping).
- **GW-07**: all live gateway error paths now route through the shared
mapper — `envelope_to_response` and `forbidden_response` call the new
`call_error_to_http_response_with_identity` (added in error.rs,
identity-aware variant of the existing builder that keeps the 401/403
and 401/422 splits). Test: a retryable `HTTP_503` handler error on
`/call` carries `Retry-After: 30`.
- **GW-12**: behavior unchanged (200-on-stream per ADR-049); the
asymmetry is now documented in http-server.md's streaming section
("Status-code asymmetry on /subscribe").
- **GW-13**: `subscribe_handler` returns
`Sse::new(stream).keep_alive(KeepAlive…15 s)` with a `retry:` field
(15000 ms) on every stream event and on the keep-alive comment frame
itself (axum's event-based keep-alive re-sends the full event, so
quiet streams still deliver a `retry` hint). Interval is the
`SSE_KEEP_ALIVE_INTERVAL` constant (15 s), documented in http-server.md.
- **GW-14**: routes.rs module doc rewritten (6 endpoints, `/publish` in
this module, stale "separate module" claim removed). AGENTS.md §7's
"5 gateway endpoints" framing was NOT edited — AGENTS.md is a parallel
read for other agents; the operative count is documented in
http-server.md and ADR-068 (AGENTS.md alignment can ride a docs-only
commit).
Verification: cargo test (243 passed, incl. 9 new); cargo clippy
--all-targets -- -D warnings clean; cargo fmt --check clean.