Skip to content

HTTP/3 (QUIC) foundation: Phase 6 (Stream layer and flow control) - #27882

Open
quaesitor-scientiam wants to merge 2 commits into
vlang:masterfrom
quaesitor-scientiam:http3-quic-stream-layer
Open

HTTP/3 (QUIC) foundation: Phase 6 (Stream layer and flow control)#27882
quaesitor-scientiam wants to merge 2 commits into
vlang:masterfrom
quaesitor-scientiam:http3-quic-stream-layer

Conversation

@quaesitor-scientiam

@quaesitor-scientiam quaesitor-scientiam commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Summary

Phase 6 of HTTP/3/QUIC support (#27675): the stream layer and flow
control, continuing from the completed Phase 5 work in #27881 (full
handshake completion). Builds on the completed Phase 0-5 foundation —
#27680, #27877, #27880, #27881 — all merged, and targets master directly.

See vlib/net/quic/PROGRESS.md
for the exact phase-by-phase checklist.

Scope

  • frame.v extended with every stream-related frame: STREAM, RESET_STREAM,
    STOP_SENDING, MAX_DATA, MAX_STREAM_DATA, MAX_STREAMS, DATA_BLOCKED,
    STREAM_DATA_BLOCKED, STREAMS_BLOCKED.
  • stream.vQuicStream, stream ID categories (RFC 9000 §2.1:
    client/server-initiated, bidirectional/unidirectional, encoded in the
    low 2 bits), independent send/receive state machines per bidi stream.
    Even this client-first phase must correctly receive server-initiated
    unidirectional streams from day one -- HTTP/3's control stream and
    QPACK encoder/decoder streams are all server-to-client uni streams,
    not deferrable to a later server-support phase.
  • flow_control.v — connection- and stream-level windows enforced
    together (a frame within its own stream's window can still be blocked
    by the connection-level aggregate window and vice versa); three
    separate stream-level initial limits depending on who opened the stream
    and its directionality; auto-window-growth heuristic to avoid
    throughput stalls; RESET_STREAM's Final Size reconciled against
    previously-received offsets (FINAL_SIZE_ERROR on mismatch).
  • stream_reassembly.v — per-stream offset-ordered reassembly, mirroring
    Phase 4's crypto_stream.v pattern but for STREAM frames' implicit-length-
    at-end-of-packet handling.

Test plan

  • Stream-ID category derivation (all 4 combinations) -- both directions
    (id -> category and category -> first id).
  • Peer-initiated uni-stream acceptance -- the plan's own explicitly
    named test, confirming a server-initiated uni stream has exactly a
    receive half and no send half from the client's perspective, plus
    exercised end-to-end in the integration test below.
  • Connection-vs-stream window interplay -- both directions (a tight
    connection window blocking a send the stream window alone would
    allow, and vice versa).
  • FINAL_SIZE_ERROR on conflicting final size -- covers a final size
    smaller than already-received data, a final size that later changes,
    a final size conflicting with an already-buffered out-of-order
    fragment, and data arriving after the final size that would exceed it.
  • Multi-stream interleaved integration test -- three streams (client
    bidi, server-initiated uni, second client bidi), STREAM frames
    delivered genuinely interleaved and out of order, each independently
    reassembled while one connection-level window tracks the running
    total across all three.
  • All new/modified code passing under ./vnew test <path> -- 28/28
    files in vlib/net/quic/.
  • ./vnew fmt -w applied to all touched .v files.
  • Branch rebased onto upstream/master (2026-08-07) now that HTTP/3 (QUIC) foundation: Phase 5 (Full handshake completion) #27881
    merged (also via squash) -- clean git rebase --onto, no test
    adaptation needed (unlike HTTP/3 (QUIC) foundation: Phase 5 (Full handshake completion) #27881's rebase, no upstream validation
    change broke this branch's own tests).

/vreview (full A-G pass, run once while writing this phase and again
after the rebase before push) found and fixed:

Before commit: QuicStream.send/recv were Optional VALUE fields, so
mutating the unwrapped copy via note_data()/note_size_known() looked
like in-place mutation but silently didn't persist unless the caller
remembered to reassign it back (s.recv = recv) -- a future caller could
easily miss this and get stale state/final_size while the underlying
data was still correct. Fixed by switching to nilable pointers (matching
Tls13ClientHandshake.verified_chain's established convention) before any
real caller could hit it, eliminating the bug class rather than
documenting the trap. Also fixed two mechanical V-compiler quirks along
the way: match on a repeated array-index expression (frames[N]) stops
reliably narrowing a sum type once it has enough variants -- affected two
pre-existing tests in frame_test.v/initial_exchange_test.v that had
worked fine with fewer variants -- and a pre-existing "frame type 0x08 is
unimplemented" test that became false once this phase implemented STREAM
frames at that exact value (retargeted to 0x1e/HANDSHAKE_DONE).

After the rebase, before push: 2 confirmed sibling-parity gaps, both
citations verified against the primary RFC 9000 text before fixing.
parse_stream_frame/encode_stream_frame were missing RFC 9000 §19.8's
"offset + length ≤ 2^62-1" bound -- identical to §19.6's CRYPTO-frame
requirement, which parse_crypto_frame/encode_crypto_frame already
correctly enforce in this same file. parse_max_streams_frame/
encode_max_streams_frame were missing RFC 9000 §4.6's 2^60 cap --
already correctly enforced (with its own boundary tests) on the
transport-parameter half of the identical requirement in
transport_parameters.v. Fixed both, 5 new regression tests. Also
documented (not changed, since it's plausibly a future Phase 9 caller's
responsibility): open_local_stream has no max_streams enforcement,
unlike its receive-side sibling get_or_create.

🧙 Built with WOZCODE

quaesitor-scientiam and others added 2 commits August 7, 2026 22:53
Extends frame.v with all stream-related frames (STREAM, RESET_STREAM,
STOP_SENDING, MAX_DATA, MAX_STREAM_DATA, MAX_STREAMS, DATA_BLOCKED,
STREAM_DATA_BLOCKED, STREAMS_BLOCKED). Adds stream.v (stream ID
categories, send/recv state machines, QuicStreamSet), stream_reassembly.v
(per-stream offset-ordered reassembly with final-size reconciliation,
mirroring Phase 4's crypto_stream.v), and flow_control.v (connection-
and stream-level windows, the RFC 9000 §4.1 initial-limit naming
inversion resolved in one place).

/vreview found and fixed a real design footgun: QuicStream.send/recv
were Optional value fields, so mutating the unwrapped copy via
note_data()/note_size_known() silently didn't persist unless the
caller remembered to reassign it back. Fixed by switching to nilable
pointers (matching Tls13ClientHandshake.verified_chain's established
convention) before any real caller could hit it. Also fixed two
mechanical V-compiler quirks: match on a repeated array-index
expression stops reliably narrowing a sum type past a certain variant
count (affected two pre-existing tests too), and a stale "type 0x08
unimplemented" test now that Phase 6 implements STREAM frames there.

Includes the plan's own named integration test: three interleaved
streams (client bidi, server-initiated uni, second client bidi) with
connection-level flow control tracked across all three.

Co-Authored-By: WOZCODE <contact@withwoz.com>
…EAMS frames

/vreview pass on this branch after rebasing onto master (Phase 5 merged):
found and fixed 2 confirmed sibling-parity gaps before push.

- frame.v: parse_stream_frame/encode_stream_frame were missing the
  "offset + length <= 2^62-1" bound RFC 9000 §19.8 requires (identical to
  §19.6's CRYPTO-frame requirement, which parse_crypto_frame/
  encode_crypto_frame already correctly enforce in this same file). A
  STREAM frame with offset near 2^62-1 and nonzero data previously parsed
  successfully with no FRAME_ENCODING_ERROR, only accidentally caught
  downstream by stream_reassembly.v's unrelated 1 MiB DoS cap.

- frame.v: parse_max_streams_frame/encode_max_streams_frame had no check
  against RFC 9000 §4.6's 2^60 cap (verified against the primary RFC
  text). transport_parameters.v already enforces this exact limit on the
  transport-parameter half of the same RFC requirement, with full
  boundary tests -- the frame half, added in this same PR, wasn't checked
  against that in-repo sibling before this pass.

Also documented (not changed): stream.v's open_local_stream has no
max_streams enforcement, unlike its receive-side sibling get_or_create --
plausibly a future Phase 9 caller's responsibility (it owns the peer's
currently-advertised limit), but wasn't documented as deferred the way
other genuinely-deferred hooks in this file are. Added that note.

5 new regression tests (STREAM offset+length bound: encode, parse with
explicit length, parse with implicit/LEN-bit-clear length; MAX_STREAMS
2^60 bound: encode, parse, boundary-accepted). Full suite: 28/28.

Co-Authored-By: WOZCODE <contact@withwoz.com>
@quaesitor-scientiam
quaesitor-scientiam marked this pull request as ready for review August 8, 2026 03:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant