fix(gateway): unify INVALID_INPUT to 422 on hand-rolled paths + sink deadline (GW-16, GW-17)
GW-16: empty body / malformed first line / missing header fields / per-line cap / batch over-cap rejections now route through call_error_to_http_response_with_identity, mapping INVALID_INPUT to 422 — same status as mid-stream chunk errors. One error class, one status. GW-17: invoke_sink wraps the registry sink invoke in the same 30 s tokio::time::timeout the Once-op invoke uses; a hung sink handler surfaces as a TIMEOUT (504, retryable) error envelope instead of holding the HTTP connection forever. The sink wrapper bounds the whole dispatch (chunk pacing included), matching http-server.md's deadline contract. Docs: http-server.md error table documents the 422 triggers and the sink deadline; http-adapters.md batch cap status corrected. to_openapi: gateway spec version 1.3.0 -> 1.4.0 (ADR-045 minor): /publish framing faults and /batch cap reject documented at 422 (the 400 slots moved with the runtime); /publish 400 slot removed; 504 description covers the sink dispatch. Verification: scripts/verify.sh OK (397 tests); cargo test --all-features OK (513 tests); clippy --all-features --all-targets -D warnings OK; cargo fmt --check OK.
This commit is contained in:
@@ -545,7 +545,9 @@ adapter contract from alkcall ADR-022 faithful on the error axis — no
|
||||
silent dropping of error contracts. The `/batch` endpoint documents no
|
||||
HTTP 500 (review-002 PRJ-21): every per-entry dispatch failure is an
|
||||
in-band `results[]` entry; the only HTTP error status is the
|
||||
request-level cap failure (400). `BatchResultEntry.error` references
|
||||
request-level cap failure (422; GW-16 unified it with the
|
||||
`INVALID_INPUT → 422` mapping — it was hand-rolled as 400 before
|
||||
review-002). `BatchResultEntry.error` references
|
||||
the `BatchError` component (review-002 PRJ-16b): the serialized
|
||||
`CallError` as a oneOf over the six protocol-code envelopes plus a
|
||||
generic arm carrying the operation-declared codes. See alkcall ADR-016.
|
||||
|
||||
@@ -217,7 +217,7 @@ the response is `text/event-stream` (negotiated via
|
||||
check before dispatch. The two methods diverge only on the return shape
|
||||
(stream vs single envelope). Streaming invokes set `deadline: None` —
|
||||
subscriptions are unbounded by contract, unlike the 30 s gateway
|
||||
deadline on Once-op invokes (see Error Mapping below).
|
||||
deadline on Once-op and sink invokes (see Error Mapping below).
|
||||
- For each `ResponseEnvelope` the stream yields, writes an SSE `data:` frame:
|
||||
`Ok(value)` → `data:` frame with the output serialized as JSON; `Err` →
|
||||
SSE error event with the `CallError` serialized, then close (an `Err` is
|
||||
@@ -358,7 +358,7 @@ Schemas") map to HTTP status codes:
|
||||
|-------------|-------------|-------|
|
||||
| `NOT_FOUND` (operation not registered, or Internal op) | `404` | |
|
||||
| `FORBIDDEN` (insufficient scopes, or unauthenticated) | `401` (no token) / `403` (token present) | |
|
||||
| `INVALID_INPUT` (schema mismatch) | `422` | |
|
||||
| `INVALID_INPUT` (input-data fault) | `422` | one status for every trigger: schema mismatch, `/publish` NDJSON framing faults (empty body, malformed first line, missing `operation`/`chunk`, per-line cap, body-read failure), and the `/batch` over-cap reject (`INVALID_INPUT` is an input-data fault wherever it fires — GW-16 unified the formerly hand-rolled 400s onto this row) |
|
||||
| `INVALID_OPERATION_TYPE` (wrong dispatch path for the op's type) | `422` (token present) / `401` (no token) | consistent across `/call`, `/batch`, `/publish` — a client fault, never a server fault |
|
||||
| `TIMEOUT` | `504` | `retryable: true` |
|
||||
| `INTERNAL` | `500` | |
|
||||
@@ -379,11 +379,15 @@ ADR-016) and `from_openapi`-imported codes are prefixed `HTTP_<status>`
|
||||
to avoid collision with protocol codes.
|
||||
|
||||
**Per-endpoint dispatch deadline.** Once-op invokes (`/call`, `/batch`
|
||||
entries, `/search`, `/schema`, and the `/publish` final envelope) are
|
||||
bounded by a 30 s gateway deadline (`GatewayDispatch::invoke` wraps the
|
||||
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 —
|
||||
entries, `/search`, `/schema`) and sink invokes (the `/publish` final
|
||||
envelope) are bounded by the 30 s gateway deadline
|
||||
(`GatewayDispatch::invoke` and `GatewayDispatch::invoke_sink` wrap the
|
||||
registry invoke in `tokio::time::timeout`, GW-17); a hung handler —
|
||||
Once or sink — surfaces as a `TIMEOUT` error (`504`, `retryable:
|
||||
true`), not an indefinitely-held HTTP request. The sink wrapper bounds
|
||||
the whole dispatch (chunk pacing included), so the final envelope
|
||||
always arrives, or the deadline trips, within the window. Streaming
|
||||
invokes (`/subscribe`) are unbounded —
|
||||
subscriptions are long-lived by contract (alkcall ADR-021 sets
|
||||
`deadline: None` for the streaming branch). The same time/bytes split
|
||||
governs the **outbound** half of an imported subscription:
|
||||
|
||||
Reference in New Issue
Block a user