Files
alkhttp/tasks/gateway/review-001-gateway-stream-errors.md
T
glm-5.3-flash 9bc9e669e1 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.
2026-08-29 10:13:03 +00:00

6.3 KiB

id, name, status, depends_on, scope, risk, impact, level, tags
id name status depends_on scope risk impact level tags
review-001-gateway-stream-errors Gateway error-fidelity — SSE terminality, mappings, Retry-After, deadline (GW-03..GW-07, GW-12..GW-14) completed
narrow low component implementation
gateway
review-001

Description

Review 001 gateway error-fidelity findings (src/gateway/routes.rs, error.rs, dispatch.rs) — the response a caller sees must match the documented contract and match across transports:

  • GW-04: SSE error events are not terminal — the stream continues after Err (routes.rs:299-313) while the wire dispatcher treats Err as terminal; two transports disagree about stream semantics (http-server.md:219-223 documents terminal). Emit the error frame and end the stream (take_while).
  • GW-03: INVALID_OPERATION_TYPE → 500 on /call///batch (client fault reported as server fault; pollutes alerting) but 400 on /publish. Map consistently (400 or 422) and document in http-server.md.
  • GW-05: the 30 s DEFAULT_TIMEOUT deadline is recorded (dispatch.rs:34,165) but never enforced — a hung handler holds the request open indefinitely. Either enforce (tokio::time::timeout around Once-op invokes) or remove the dead metadata. Prefer enforcing; it is the documented contract.
  • GW-07: Retry-After machinery exists in call_error_to_http_response (error.rs:61-75) but the main gateway error path builds responses by hand (routes.rs:319-333) — retryable HTTP_429/HTTP_503 reach callers with no Retry-After despite the documented mapping. Route gateway error responses through the shared mapper.
  • GW-12: ACL denial on /subscribe surfaces as HTTP 200 + event:error while /call returns 401/403. ADR-049 makes 200-on-stream defensible, but the doc must call out the asymmetry — coordinate with review-001-output-projection (PRJ-05) which documents it.
  • GW-13: no SSE keep-alive/heartbeat (routes.rs:172) — quiet-but- alive streams (the normal state for subscriptions) die at LB/proxy idle timeouts. Add keep-alive + retry: field.
  • GW-14: stale module doc (routes.rs:1-9 claims /publish is "a separate module"; AGENTS.md §7's "5 gateway endpoints" framing is similarly stale — ADR-068 made it 6).

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

References

  • docs/reviews/001-initial-implementation-review.md (Part C, GW-03..GW-07, GW-12..GW-14)
  • docs/architecture/decisions/049-streaming-handler-for-subscriptions.md
  • docs/architecture/decisions/023-operation-error-schemas.md

Notes

Agent fills during implementation. Independent of 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

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