fix(gateway): publish validation + streaming + batch semantics (GW-01, GW-06, GW-08..GW-11, HY-13)
- GW-01: /publish validates every NDJSON chunk against the op's publish_schema (incl. the first-line chunk) via NdjsonChunkStream — terminal Err(INVALID_INPUT)/422 on violation, matching the wire dispatcher's per-chunk contract. Route-level fix; the alkcall spine was explored and rejected (wire validation is pump-side by design). - GW-06: the body is streamed, not buffered — Body::into_data_stream() -> newline-framed BufferedLines -> lazily parsed chunk stream. ADR-068 documents the streamed semantics and the 2 MiB per-line cap. - GW-08: /batch capped at 100 operations (INVALID_INPUT 400). - GW-09: internal-op batch entries now carry generated UUID request ids. - GW-10: first publish line missing `chunk` is rejected INVALID_INPUT. - GW-11: redundant /publish pre-checks removed; enforcement rides on invoke_sink via the shared dispatch spine. - HY-13: the vacuous stub test was replaced by a body-cut-short test. - Adjacent: INVALID_OPERATION_TYPE now maps 422 (with identity) / 401 (without) in error.rs — the route relies on the shared mapper since the pre-checks are gone (GW-03's finding; was a 500 fall-through). Verification: cargo test 211 passed; cargo clippy --all-targets -- -D warnings clean; cargo fmt --check clean.
This commit is contained in:
@@ -67,6 +67,23 @@ stream; the operation's final `ResponseEnvelope` is the HTTP response.
|
||||
([ADR-023](023-operation-error-schemas.md), the gateway's
|
||||
`HTTP_<status>` fidelity rules).
|
||||
|
||||
### Body handling (streamed, not buffered)
|
||||
|
||||
The NDJSON body is **streamed, never fully buffered** (GW-06): axum's
|
||||
`Body` is framed into lines as bytes arrive, and each line is parsed and
|
||||
pushed into the sink lazily — memory is bounded by the per-line cap (2
|
||||
MiB, matching axum's default whole-body limit), not by the unbounded
|
||||
chunk count. `publish_schema` validation is applied per chunk inside the
|
||||
sink-feeding stream (GW-01), so a Pub op registered with a
|
||||
`publish_schema` enforces the same per-chunk contract over HTTP as over
|
||||
the call protocol; a violation terminates the chunk stream with
|
||||
`INVALID_INPUT` (→ 422), the exact item shape an initiator-side
|
||||
`call.error` produces on the wire. A first line missing `chunk` (GW-10)
|
||||
is rejected `INVALID_INPUT` before dispatch — indistinguishable from a
|
||||
missing `operation`. The client disconnect abort path is unchanged: the
|
||||
dropped body stream propagates EOF through the framed reader into the
|
||||
sink's `PublishStream`.
|
||||
|
||||
### Wire-shape note
|
||||
|
||||
On the call protocol, the initiator's chunks are `call.published`
|
||||
@@ -105,6 +122,10 @@ the doc does not preload operations.
|
||||
- The dispatch path is `invoke_sink()` — the same spine as `/call`'s
|
||||
`invoke()`, so the shared-dispatch invariants (identity, ACL,
|
||||
Internal filtering) hold by construction.
|
||||
- The body is streamed line-by-line (memory bounded by the per-line
|
||||
cap; backpressure inherited from the HTTP body), so a client
|
||||
disconnect mid-stream cancels the sink exactly like a dropped
|
||||
write-half on the call protocol.
|
||||
|
||||
**Negative:**
|
||||
|
||||
@@ -118,6 +139,11 @@ the doc does not preload operations.
|
||||
the `/openapi.json` version bumps~~ — settled: first-line
|
||||
`{operation, chunk}` convention; terminal errors are plain HTTP
|
||||
status + JSON body (not an NDJSON line).
|
||||
- The 2 MiB per-line cap (not a whole-body cap) bounds a single chunk;
|
||||
the total number of chunks is unbounded. Handlers that would receive
|
||||
unbounded streams over the wire get the same behavior over HTTP —
|
||||
operators front the endpoint with the same body/timeout controls used
|
||||
for any other streaming surface.
|
||||
|
||||
## References
|
||||
|
||||
|
||||
Reference in New Issue
Block a user