Skip to content

[P0] Stream generic gateway upstream responses with bounded backpressure #442

Description

@seonghobae

Verified buyer/security gap

Fresh protected-source review on main@f8260f1e03836039ff9463dd99fa982e4e270c4b confirms that Wardnet's generic proxy_request() sends the upstream request with reqwest and then materializes the upstream response before returning it through Axum. Issue #440 independently records the same current behavior as "reads only the upstream status and complete body bytes" and explicitly requires unresolved response streaming to be tracked as a dependent gateway gap.

This is independent of #440's header-mediation defect: even after end-to-end fields are mediated correctly, whole-response buffering allows an admitted upstream to turn one buyer request into unbounded/large resident memory, delays first byte until the response completes, defeats natural downstream backpressure, and makes long-lived/chunked/SSE-style responses non-viable. It is also distinct from #89's contextual-orchestrator-specific LLM seam and #86's Coraza/IDS streaming requirements.

Ownership boundary

This is Wardnet-owned generic gateway/SOC control-plane relay behavior. Wardnet owns safe request admission, generic response relay, cancellation/backpressure evidence and buyer-path latency. It must not copy or reimplement EgressWeave's canonical outbound URL/address/DNS/peer/redirect/proxy/TLS/resource-authorization logic, contextual-orchestrator provider/model routing, Coraza engine internals, quarantine runtime behavior, or appguardrail policy logic.

Consume released owner contracts only. Until an immutable EgressWeave contract/release exists for transport-policy authority, keep that dependency fail-closed/documented rather than inventing a local substitute.

Hostile realistic RED

Serialize implementation behind the active gateway writers (#435 and #441/#440); do not create a competing src/lib.rs writer while either owns the same seam. The first implementation PR must keep production behavior unchanged until tests prove the defect against a loopback upstream.

RED must include at least:

  • a loopback upstream that emits an admitted response in delayed chunks and proves Wardnet currently withholds the first downstream bytes until the upstream body is complete;
  • a response larger than the bounded relay-memory budget (including missing or dishonest Content-Length) proving current whole-body materialization can exceed the intended memory envelope;
  • a slow consumer proving downstream slowness does not trigger unbounded Wardnet buffering after GREEN;
  • client disconnect/cancellation proving the upstream body is promptly dropped/cancelled rather than read to completion in the background;
  • an upstream failure after partial body delivery with deterministic observable semantics and no fabricated success/evidence;
  • concurrent slow/chunked responses proving one connection cannot cause head-of-line memory amplification across unrelated buyer traffic;
  • coexistence with [P0] Preserve bounded end-to-end HTTP headers across the gateway proxy #440's header-mediation contract: hop-by-hop/framing authority remains stripped and admitted end-to-end response metadata survives without reintroducing whole-body buffering;
  • route selection, local threat/DNSBL deny, Coraza precedence/body gates and audit semantics remain unchanged.

Runner/bootstrap/format noise is not semantic RED. The loopback client must observe the real stream/timing/cancellation assertions.

Minimum causal GREEN contract

  • Relay the admitted upstream response as a bounded asynchronous body stream through Axum (or equivalent backpressure-preserving primitive) rather than collecting the complete response into Bytes/Vec first.
  • Preserve downstream backpressure; do not add a hidden unbounded channel or queue between reqwest and Axum.
  • Propagate cancellation so a disconnected buyer stops unnecessary upstream reads promptly.
  • Define a bounded policy for per-chunk/per-response accounting, timeouts and partial-stream errors. Never convert an upstream mid-stream failure into a complete success claim.
  • Do not automatically replay a partially relayed non-idempotent request/response path.
  • Compose with the single [P0] Preserve bounded end-to-end HTTP headers across the gateway proxy #440 header-mediation boundary instead of duplicating header policy in the stream adapter.
  • Keep outbound destination/DNS/peer/redirect/proxy/TLS/resource authorization delegated to released EgressWeave contracts; streaming is not transport authorization.
  • Keep contextual-orchestrator-specific SSE/model/provider semantics in the released CO contract seam (Preserve fail-closed LLM gateway admission evidence behind contextual-orchestrator #89); this issue is protocol-generic.
  • No cross-service SQL, source copy, mutable sibling dependency, ambient-proxy widening or long DB transaction/explicit lock across upstream streaming.

Exact-head acceptance

On one unchanged candidate head require:

  • cargo fmt --check, cargo test --locked --workspace, strict Clippy, Fuzz and live security/SAST/CodeQL/governance checks;
  • 100% owned-production statement/branch/edge/public-rustdoc evidence for the new streaming boundary;
  • realistic async k6/E2E buyer-path evidence with Wardnet's standing p95 <=20 ms target for ordinary admitted responses, plus separately reported first-byte latency/stream duration for intentionally long-lived fixtures;
  • bounded-memory/backpressure evidence under concurrent large/slow/chunked responses;
  • current reviews and all threads resolved, then normal non-force merge only;
  • architecture/security/ops/test/release docs and the sole product-gap baseline updated by their current owner lane without competing edits.

Keep this issue open until the effective code/test/doc delta reaches protected main. No self/model approval, force push, destructive rebase, routine bypass, gate weakening, no-op redispatch churn, mutable dependency, or predecessor-head evidence promotion.

Traceability

Activity

  1. seonghobae commented on Sep 21, 2026

    @seonghobae
    ContributorAuthor

    2026-09-21 KST hostile RED expansion for #443.

    Fresh review kept production source byte-identical and added one real over-budget loopback fixture on test/442-gateway-streaming-red. Exact head is now 7f0e1ed021f109e45c274014e55d9c94017ac6ca: the upstream exposes an admitted 1 MiB + 64 KiB prefix with no declared length and holds its tail, and the buyer contract requires more than the explicit 1 MiB relay-memory budget to arrive before that tail is released. This prevents a dishonest-Content-Length specimen alone from standing in for actual large-body evidence.

    The predecessor 20f3fcd3a56c2d1069784cf43bd739eabfa6ea45 queued workflow set is invalidated and does not transfer. Exact 7f0e1ed021f109e45c274014e55d9c94017ac6ca currently has no materialized hosted workflow evidence, so no semantic RED/GREEN is claimed. Keep the lane Draft and test-only while #435/#441/#140 own the overlapping production seam.

    Still required before production repair: slow-consumer backpressure, post-admission disconnect/cancellation, concurrent slow/chunked isolation, and composition with #440 header mediation plus unchanged route/local-deny/Coraza/audit behavior. No EgressWeave/contextual-orchestrator/quarantine/appguardrail owner logic is duplicated.

  2. seonghobae commented on Sep 21, 2026

    @seonghobae
    ContributorAuthor

    2026-09-22 KST exact-head hostile RED is now established for #443.

    Production source remains byte-identical to protected main@f8260f1e03836039ff9463dd99fa982e4e270c4b, and #443 exact head remains eedc56c1e0fefc4654f2e1bdc8b7ceae99a28825.

    Hosted CI 35610237483 / rust job 106367237430 reached the real loopback tests after cargo fmt --check passed and all earlier workspace test suites succeeded. tests/gateway_streaming.rs executed three hostile buyer assertions and all three failed for the intended causal behavior:

    • delayed tail: Wardnet withheld the downstream response until the upstream tail was released;
    • dishonest Content-Length: Wardnet withheld the admitted prefix instead of streaming it;
    • partial upstream reset: Wardnet withheld the admitted prefix until the later upstream body failure.

    This is semantic RED, not bootstrap/runner/format noise. Security 35610237788 and SAST 35610237552 are GREEN; CodeQL 35610237414 remains queued under the central workflow settlement. The separate no-Content-Length >1 MiB bounded-memory fixture and downstream-cancellation fixture are present on this exact head, but cargo test stopped after the earlier failing integration binary, so those two fixtures are not promoted as hosted execution evidence.

    Still required before production repair: a defensible slow-consumer/backpressure witness, concurrent slow/chunked isolation, then non-force composition/restack with #440/#441 header mediation and preservation of route/local-deny/Coraza/audit semantics. Keep production repair serialized behind #435/#441/#140. Do not copy EgressWeave/contextual-orchestrator/quarantine/appguardrail owner authority, and do not use bypass/no-op dispatch to manufacture evidence.

  3. seonghobae commented on Sep 23, 2026

    @seonghobae
    ContributorAuthor

    2026-09-24 KST RED-lane expansion on #443.

    Current exact test-only head is 239c747848a8241cbbccd66e6f91d20725860c51; production source remains byte-identical to protected main@f8260f1e03836039ff9463dd99fa982e4e270c4b and the lane remains Draft behind the serialized #435/#441/#140 source writers.

    New tests/gateway_a_streaming_backpressure.rs adds the two previously missing protocol-generic witnesses without copying foreign-owner logic:

    • slow consumer/backpressure: a real loopback upstream offers a finite 64 MiB body in 64 KiB chunks. Wardnet must expose the response before body completion and, while the buyer body stays unpolled for 250 ms, bounded read-ahead must remain within the explicit 8 MiB witness rather than draining the whole body in the background;
    • concurrent held streams: four independent slow upstream responses hold their tails. All four buyer response heads/prefixes must be exposed before tail release, and unrelated fast buyer traffic must remain responsive while those streams stay open.

    Fresh exact-head CI 35880184584, Security 35880184517, SAST 35880184578, and CodeQL PR 35880184686 are queued. Therefore predecessor semantic RED validates the causal finding but is not promoted as evidence for this successor head, and the two new cases are not yet claimed RED/GREEN. No blind rerun or wake commit.

    Remaining #442 acceptance before production repair is non-force composition with the single #440/#441 header-mediation boundary plus unchanged route/local-deny/Coraza/audit semantics. Production repair remains unauthorized until that writer lane clears.

  4. seonghobae commented on Sep 24, 2026

    @seonghobae
    ContributorAuthor

    Current hostile RED is now executed on unchanged test-only #443@2cccfb37b85c3fc30d4b91d3027266feaf338774. CI 35921459298, rust job 107386159436, acquired hosted Ubuntu 24.04; checkout/toolchain/cargo fmt --check passed, all 142 existing library unit tests and pre-existing integration suites passed first, then only the new streaming/backpressure witnesses failed:

    • idle buyer body drained the entire upstream 1024 x 64 KiB (64 MiB) within the 250 ms observation window instead of applying bounded downstream backpressure;
    • four concurrent held upstreams kept all admitted buyer response heads/prefixes behind unreleased tails instead of exposing independent streams before completion.

    This is valid source-causal RED for the whole-response-buffering finding, not runner/bootstrap/format noise. SAST 35921459251 is terminal SUCCESS; Security 35921459231 and CodeQL 35921459105 remain queued. Keep production GREEN serialized behind active generic gateway mediation #441/#440, then implement one bounded async relay that composes with the single header-mediation boundary; do not copy EgressWeave transport authorization or other owner logic.

  5. seonghobae commented on Sep 24, 2026

    @seonghobae
    ContributorAuthor

    Fresh composition review now clears the former #440/#441 serialization prerequisite for #442: #441 is normally integrated into current Coraza/gateway parent #435 (534a48b54dcd9e668ceeec8d3d00f824531b0ad1), and #443 has non-force adopted that parent as exact c47b35c36eed9aad1c0968c82aa14b1c40cf352b. The parent-relative #443 delta is still only the four hostile streaming test files; no foreign-owner transport policy is present.

    The valid production finding is still exact and causal in current src/lib.rs blob e9b751c59474cae83c99d3df56a418c28e7fa083: proxy_request_with_headers() admits status/headers, then calls response.bytes().await before constructing the Axum response. That whole-body materialization directly explains the executed delayed-tail, dishonest-Content-Length, late-reset, 64 MiB idle-buyer drain, and concurrent-held-stream REDs.

    Minimum Wardnet-owned repair on #443 is deliberately narrow:

    -use axum::{
    -    Json, Router,
    -    body::Bytes,
    +use axum::{
    +    Json, Router,
    +    body::{Body, Bytes},
     ...
    -    let bytes = response
    -        .bytes()
    -        .await
    -        .map_err(|error| format!("upstream body read failed: {error}"))?;
    -    let mut response = (status, bytes).into_response();
    +    let body = Body::from_stream(response.bytes_stream());
    +    let mut response = Response::new(body);
    +    *response.status_mut() = status;
         *response.headers_mut() = admitted_response_headers;
         Ok(response)
     }

    Also update the existing truncated-upstream unit fixture from pre-response BAD_GATEWAY to admitted 200 OK followed by a downstream body-read error (using the test module's existing to_bytes helper); do not assert unstable error text. Do not add channels, eager drains, replay, timeout-success inference, EgressWeave authorization, or another header-mediation pass.

    Current #435 and #443 exact-head workflow sets are still queued/pending, so this handoff is not GREEN and does not authorize merge/no-op redispatch/bypass. Apply the causal source repair only by normal non-force branch history, then require the unchanged repaired exact head to turn the seven hostile contracts GREEN plus all then-live deterministic/security/coverage/review/thread/package/SBOM/provenance/governance gates.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions