8000 T105 — Strip Alt-Svc on the live response relay and point the HTTP checks at the live path by minimal-foundry[bot] · Pull Request #2791 · gominimal/minimal · GitHub
[go: up one dir, main page]

Skip to content

T105 — Strip Alt-Svc on the live response relay and point the HTTP checks at the live path - #2791

Merged
norrietaylor merged 3 commits into
mainfrom
agent/2782-strip-alt-svc-on-the-live-response-relay-and-poi
Oct 31, 2026
Merged

norrietaylor merged 3 commits into
mainfrom
agent/2782-strip-alt-svc-on-the-live-response-relay-and-poi

Conversation

@minimal-foundry
Copy link
Copy Markdown
Contributor

T105 — Strip Alt-Svc on the live response relay and point the HTTP checks at the live path

slice: S15
kind: task
covers: BEP-076
size: L
est: 260 code, 360 tests
executor: agent
difficulty: needs care
model: strongest
diagnostics: a box that requests through the proxy sees no Alt-Svc header in any relayed response, so a client never learns an HTTP/3 endpoint to try past the proxy; the box sees the first chunk of a response before the upstream sends the rest, as before
observability: one debug line per relayed response that carried an Alt-Svc header, naming the box and the authority, beside the flow's existing span
layers:
- relay:
- crates/bep/src/forward.rs: relay_responses reads each response head on the flow, removes every Alt-Svc header (BEP-076), writes the head, then streams the body chunks as they arrive by the response's framing (BEP-N01), for every response on a kept-alive flow; the tests alt_svc_is_stripped_from_responses, h2_prior_knowledge_preface_is_closed, prop_forwarded_head_is_canonical and prop_ambiguous_framing_is_refused drive forward::forward_requests and relay_responses, the path bep serve runs
test: cargo nextest run -p bep alt_svc_is_stripped_from_responses
test: cargo nextest run -p bep h2_prior_knowledge_preface_is_closed
test: cargo nextest run -p bep prop_forwarded_head_is_canonical
test: cargo nextest run -p bep prop_ambiguous_framing_is_refused
test: cargo nextest run -p bep streamed_response_is_not_buffered
- http:
- crates/bep/src/http.rs: the four tests of the same names, which drive relay_response_head and forward_request_head that no live path calls, go; the response-head parse and strip_alt_svc serve the live relay
test: cargo nextest run -p bep ambiguous_response_framing_is_refused
produces: none
consumes: none
proof: A response relayed to a box through the running proxy carries no Alt-Svc header, and the box still gets each chunk as the upstream sends it.
depends on:
- blocked by #2261
- blocked by #2272

Proof

A box that requests through bep serve gets each relayed response with no Alt-Svc header, on every response of a kept-alive flow (interim 1xx, HEAD replies, Content-Length and chunked bodies). The box still gets the first chunk of a streamed response before the upstream sends the rest (streamed_response_is_not_buffered runs through the live proxy).

Notes from the agent: Out of layers: crates/bep/src/proxy.rs, 3 lines in relay(). It takes flow.responses() before the request half moves the Flow, and passes it to relay_responses. This is necessary: a reply to HEAD (and a 2xx to CONNECT) can only be framed if the response half knows the request, and relay_responses had no way to learn it. Flow now carries an unbounded channel of Sent {head, connect, switches}. forward_requests sends one entry before it writes each head upstream. relay_responses gained a Responses argument (box id, authority, that channel). Framing follows RFC 9112 6.3: HEAD/1xx/204/304 have no body. 101 or 2xx to CONNECT, and a body with no framing header, are copied raw until close. A Content-Length body is copied exactly. A chunked body is re-serialized canonically, and its chunk extensions and trailers are dropped, the same as the request side. A response head or chunk that breaks the strict rules closes the flow with a debug line that names the rule. The observability line is tracing::debug with box_id, authority and status, in the box_connection span. In http.rs, the four old tests and relay_response_head are gone, and the shared proptest helpers moved to forward.rs tests. Response now carries its parsed body framing, and parse_chunk_size is pub(crate). forward_request_head and http::forward_requests stay, because other http.rs tests still use them. The proxy.rs module doc line about relay_responses is still true and was not changed. One full cargo nextest run -p bep after the commit: 234 passed.

Tests

line exit tail
cargo nextest run -p bep alt_svc_is_stripped_from_responses 0 Applying loadouts: default Running task runtime-implement-test-0 in session task-runtime-implement-test-0-3253... Compiling paths v0.0.1 (/workbench/crates/paths) Compiling client-config v0.0.1 (/workbench/crates/client-config) Compiling bep v0.0.1 (/workbench/crates/bep) Finished 'test
cargo nextest run -p bep h2_prior_knowledge_preface_is_closed 0 Applying loadouts: default Running task runtime-implement-test-1 in session task-runtime-implement-test-1-4605... Finished 'test' profile [unoptimized] target(s) in 0.23s ──────────── Nextest run ID 4bf5410c-d93e-4aa0-a943-c4e593ad3b65 with nextest profile: default Starting 1 test across 2
cargo nextest run -p bep prop_forwarded_head_is_canonical 0 Applying loadouts: default Running task runtime-implement-test-2 in session task-runtime-implement-test-2-09bc... Finished 'test' profile [unoptimized] target(s) in 0.25s ──────────── Nextest run ID c045c352-00ab-4b19-8490-94f084fa1a56 with nextest profile: default Starting 1 test across 2
cargo nextest run -p bep prop_ambiguous_framing_is_refused 0 Applying loadouts: default Running task runtime-implement-test-3 in session task-runtime-implement-test-3-52ce... Finished 'test' profile [unoptimized] target(s) in 0.23s ──────────── Nextest run ID 23ebfff9-be3e-4ab3-ab10-514b2c5fae17 with nextest profile: default Starting 1 test across 2
cargo nextest run -p bep streamed_response_is_not_buffered 0 Applying loadouts: default Running task runtime-implement-test-4 in session task-runtime-implement-test-4-dd70... Finished 'test' profile [unoptimized] target(s) in 0.23s ──────────── Nextest run ID c25c6749-4dd2-465d-97c8-508719958d7b with nextest profile: default Starting 1 test across 2
cargo nextest run -p bep ambiguous_response_framing_is_refused 0 Applying loadouts: default Running task runtime-implement-test-5 in session task-runtime-implement-test-5-e2e7... Finished 'test' profile [unoptimized] target(s) in 0.23s ──────────── Nextest run ID 24a80acc-9144-4470-9dba-2685eccff479 with nextest profile: default Starting 1 test across 2

Verify

min task run agent-verify passed ([tasks.verify]).

Out of scope

These files are outside the task's layers:, so this PR is a draft:

  • crates/bep/src/proxy.rs

Closes #2782

relay_responses now parses each response head on the flow, removes
every Alt-Svc header and writes the canonical head. It then relays the
body by the response's framing, and flushes each part as it arrives.
It does this for every response on a kept-alive flow. Flow hands the
response half each forwarded request, so a reply to HEAD is framed
right.

The HTTP head tests now drive forward_requests and relay_responses,
the path bep serve runs. relay_response_head, which no live path
called, is gone.
@coderabbitai
coderabbitai Bot commented Oct 31, 2026 •
Copy link
Copy Markdown
Contributor

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 2acd7553-c47b-475f-abf9-8624bc0ec568

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
8000

@minimal-foundry
minimal-foundry Bot commented Oct 31, 2026 •
Copy link
Copy Markdown
Contributor Author

Approved: every review thread resolved at 4b84400dd54c.

@norrietaylor resolved the last thread, which releases the approval this review had withheld.

Nothing to do.

@minimal-foundry minimal-foundry Bot left a comment
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minimal-review — advisory review

Verdict COMMENT — 1 finding, 1 inline.

Panel: 3 of 3 personas ran (correctness, security, supply-chain).

Comment thread crates/bep/src/forward.rs Resolved
@minimal-arch minimal-arch Bot left a comment
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

arch-review: CONDITIONS · 1 condition · the full ruling

The live relay now strips every Alt-Svc field and streams each body part as it arrives. The placement in bep conforms. One path fails open. ResponseRelay::run opens a raw tunnel on any 101, also when the request did not ask to switch. Later responses on the flow then reach the box unparsed, with Alt-Svc. Section 5.7 line 219 says the BEP must strip Alt-Svc from upstream responses. Make the change in C1.

Ruled at a6578aa against gominimal/arch@8640f85e193d by claude-opus-5-5.

Comment thread crates/bep/src/forward.rs Resolved
@norrietaylor norrietaylor left a comment
8000 Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes at a6578aa.

An unsolicited 101 turns the relay into a raw tunnel, and Alt-Svc gets through. crates/bep/src/forward.rs:1390.

  • ResponseRelay::run calls self.tunnel() on any 101. It never checks that the matching request asked to switch protocols.
  • Sent.switches is set at :770, and its doc comment at :762 says it marks the last request on the flow, but nothing reads it.
  • Failure: the box sends GET /a with no Upgrade. The upstream replies HTTP/1.1 101 Switching\r\n\r\n and then HTTP/1.1 200 OK\r\nAlt-Svc: h3=":443"\r\nContent-Length: 0\r\n\r\n. Everything after the 101 is copied unparsed, so the Alt-Svc reaches the box (BEP-076).
  • The request half keeps forwarding and parsing new requests, because it stops only after a request that asks to switch. So a hostile upstream can make the two halves lose step.
  • Reproduced with a throwaway live-path test: the box received both responses verbatim, Alt-Svc included.
  • Fix: enter the tunnel on 101 only when request.switches is true. Otherwise refuse the response and close the flow (for example self.broken("unsolicited_switch")). Add a live-path test that pins the refusal and that no later byte reaches the box.

Test gaps, worth closing in the same round:

  • No live-path test covers the relay's own refusals: a bad chunk size, a missing CRLF after chunk data, an oversized trailer, or EOF mid-chunk. The 204, 304 and close-delimited branches are also untested.
  • The last assertion in h2_prior_knowledge_preface_is_closed, relayed(responses, b"").is_empty(), passes whatever the relay does.

Checked and correct:

  • Alt-Svc is removed case-insensitively, from repeated headers, on interim 1xx and on every response of a kept-alive flow.
  • Obs-fold is refused, and so is whitespace before the colon. Trailers are dropped.
  • CL with TE, duplicate CL and invalid CL are refused before any byte reaches the box.
  • The head is capped at 64 KiB. The chunk-size line is bounded.
  • Pipelined pairing stays in order.
  • Each read is flushed before the next upstream read (BEP-N01).
  • I found no panic reachable from upstream input.
  • All six named tests pass, and so does the bep crate (233 run).

Arch (local build:arch-review at arch 8640f85e): PASS. BEP-076 conforms, with no conditions.

The response relay opened a raw tunnel on any 101. A hostile upstream
could then send later responses unparsed, with their Alt-Svc headers.
The relay now tunnels on a 101 only when the request asked to switch,
and otherwise closes the flow before a byte reaches the box.

Adds live-path tests for the refusal, a solicited switch, the chunked
body refusals, and the 204, 304 and close-delimited branches. The h2
preface test now checks that no request reached the response half.
@minimal-foundry
Copy link
Copy Markdown
Contributor Author

Round 1 of 10 on a review requesting changes, pushed as 4b84400dd54c.

line exit tail
cargo nextest run -p bep alt_svc_is_stripped_from_responses 0 Applying loadouts: default Running task runtime-implement-test-0 in session task-runtime-implement-test-0-75db... Compiling paths v0.0.1 (/workbench/crates/paths) Compiling client-config v0.0.1 (/workbench/crates/client-config) Compiling bep v0.0.1 (/workbench/crates/bep) Finished 'test
cargo nextest run -p bep h2_prior_knowledge_preface_is_closed 0 Applying loadouts: default Running task runtime-implement-test-1 in session task-runtime-implement-test-1-2153... Finished 'test' profile [unoptimized] target(s) in 0.27s ──────────── Nextest run ID 150fd5d5-82cf-4ec8-ae79-bb864b802513 with nextest profile: default Starting 1 test across 2
cargo nextest run -p bep prop_forwarded_head_is_canonical 0 Applying loadouts: default Running task runtime-implement-test-2 in session task-runtime-implement-test-2-69e9... Finished 'test' profile [unoptimized] target(s) in 0.29s ──────────── Nextest run ID d866fdcc-a664-49f4-b617-75144a756e53 with nextest profile: default Starting 1 test across 2
cargo nextest run -p bep prop_ambiguous_framing_is_refused 0 Applying loadouts: default Running task runtime-implement-test-3 in session task-runtime-implement-test-3-3a10... Finished 'test' profile [unoptimized] target(s) in 0.29s ──────────── Nextest run ID 1e471cce-0560-4bdf-a4bb-f81877097d41 with nextest profile: default Starting 1 test across 2
cargo nextest run -p bep streamed_response_is_not_buffered 0 Applying loadouts: default Running task runtime-implement-test-4 in session task-runtime-implement-test-4-6ae5... Finished 'test' profile [unoptimized] target(s) in 0.30s ──────────── Nextest run ID 928cf8b7-6ef2-4a3d-8111-1cea5ea44bd8 with nextest profile: default Starting 1 test across 2
cargo nextest run -p bep ambiguous_response_framing_is_refused 0 Applying loadouts: default Running task runtime-implement-test-5 in session task-runtime-implement-test-5-10b2... Finished 'test' profile [unoptimized] target(s) in 0.30s ──────────── Nextest run ID c18fdabe-d70f-41f2-a62d-092f917293a1 with nextest profile: default Starting 1 test across 2

Verify min task run agent-verify passed.

Outside the task's layers::

  • crates/bep/src/proxy.rs

Converted to a draft: something above needs a person before review.

Review threads the coder judged:

  • 👍 valid — crates/bep/src/forward.rs:772: Sent::switches is now read: the response relay opens a tunnel on a 101 only when the request asked to switch, and closes the flow on any other 101 (unsolicited_switch).
  • 👍 valid — crates/bep/src/forward.rs:1445: A 101 to a request without Upgrade or CONNECT now returns Err(self.broken("unsolicited_switch")) before any byte reaches the box. unsolicited_switch_is_refused pins it, with a later Alt-Svc response that the box does not get.

A response relayed to a box through the running proxy carries no Alt-Svc header, and the box still gets each chunk as the upstream sends it. An upstream can no longer get past the strip with a 101 that the request did not ask for: the flow closes, and no byte of that 101 or of a later response reaches the box.

Notes from the agent: Revise round 1. ResponseRelay::run now refuses a 101 when Sent.switches is false, with the rule unsolicited_switch, before it writes any byte of the head. A 101 to a request that sends Upgrade, or to CONNECT, still opens the tunnel. New live-path tests: unsolicited_switch_is_refused (it fails with the check disabled, and passes with it), solicited_switch_opens_the_tunnel, broken_chunked_body_stops_the_relay (bad chunk size, no CRLF after chunk data, oversized trailer, EOF in a chunk), and bodiless_and_close_delimited_responses_are_relayed (204, 304, close-delimited). The last assertion of h2_prior_knowledge_preface_is_closed now checks that the request half sent no request record to the response half. Before, it relayed empty upstream bytes, and that check passed whatever the relay did. When a chunked body breaks the rules, the box keeps the head and the chunk bytes that arrived before the break. This is unchanged, and the tests pin it. cargo clippy -p bep --all-targets is clean. I did not run the whole crate suite or just clippy-strict.

@minimal-foundry
minimal-foundry Bot marked this pull request as draft October 31, 2026 16:19
@minimal-arch minimal-arch Bot left a comment
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

arch-review: PASS · the full ruling

The live response relay strips every Alt-Svc header, as §5.7 line 219 requires. The fix for the earlier ruling is correct: a 101 now opens a tunnel only when the request asked to switch. Any other 101 closes the flow before a byte reaches the box. The tests drive forward_requests and relay_responses, which is the path that bep serve runs. No condition is open.

Ruled at 4b84400 against gominimal/arch@8640f85e193d by claude-opus-5-5.

@norrietaylor
norrietaylor marked this pull request as ready for review October 31, 2026 16:39
@norrietaylor norrietaylor left a comment
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving at 4b84400. Commit 4b84400 fixes the finding from my last review: ResponseRelay::run now refuses a 101 unless the request asked to switch (unsolicited_switch). It also adds the live-path tests I asked for: the unsolicited and solicited switch, the four chunked-body refusals, the 204, 304 and close-delimited branches, and a meaningful h2 preface assertion. The merge of main brings no change of its own. The arch PASS (BEP-076) carries forward, because the fix only narrows when the relay stops parsing. The proxy.rs call-site change was accepted by the owner earlier, so I marked the PR ready again.

@minimal-foundry minimal-foundry Bot left a comment
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minimal-review — approval withheld

Verdict COMMENT — APPROVE withheld: 2 unresolved threads — minimal-foundry[bot] crates/bep/src/forward.rs:772, minimal-arch[bot] crates/bep/src/forward.rs:1445. No findings.

Panel: 3 of 3 personas ran (correctness, security, supply-chain).

@minimal-foundry minimal-foundry Bot left a comment
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minimal-review — approved

Verdict APPROVE — every review thread is resolved; @norrietaylor closed the last one. The review of 4b84400dd54c withheld this approval on the threads open then. Posted as a comment: the App cannot approve its own pull request; the check and the label carry it.

@minimal-foundry minimal-foundry Bot added the review:approved minimal-review approved the current head label Oct 31, 2026
@norrietaylor
norrietaylor added this pull request to the merge queue Oct 31, 2026
Merged via the queue into main with commit e19410b Oct 31, 2026
36 checks passed
@norrietaylor
norrietaylor deleted the agent/2782-strip-alt-svc-on-the-live-response-relay-and-poi branch October 31, 2026 18:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review:approved minimal-review approved the current head

Projects

None yet

Development

Successfully merging this pull request may close these issues.

T105 — Strip Alt-Svc on the live response relay and point the HTTP checks at the live path

1 participant

0