sozu_lib/protocol/mux/h2.rs
1//! H2 mux connection wrapper (RFC 9113).
2//!
3//! Owns wire-side connection state: HPACK encoder/decoder, peer settings,
4//! flow window, GOAWAY/RST attribution, and orchestrates the
5//! [`h2_flood_detector::H2FloodDetector`] backing the CVE-2023-44487 /
6//! CVE-2024-27316 / CVE-2025-8671 mitigations. Stream
7//! storage lives in the sibling `Context<L>` (`mux/mod.rs`); this module is
8//! the canonical home for the edge-trigger discipline — paths that queue
9//! bytes for a later event-loop pass must arm writable / signal pending
10//! write (cf. `arm_writable()` at the deferred-control-frame sites and
11//! `Readiness::arm_writable` / `Readiness::signal_pending_write` in `lib/src/lib.rs`).
12
13use std::{
14 cell::Cell,
15 cmp::min,
16 collections::BTreeMap,
17 io::{IoSlice, Write as _},
18 time::{Duration, Instant},
19};
20
21/// Compile-time guard: `payload_len as usize` casts in the H2 parser assume at
22/// least 32-bit pointer width. This prevents silent truncation on platforms
23/// with smaller pointers (e.g. 16-bit embedded targets).
24const _: () = assert!(
25 std::mem::size_of::<usize>() >= 4,
26 "sozu requires at least 32-bit pointers"
27);
28
29use rusty_ulid::Ulid;
30use sozu_command::{logging::ansi_palette, ready::Ready};
31
32use crate::metrics::names;
33use crate::{
34 L7ListenerHandler, ListenerHandler, Protocol, Readiness, SessionMetrics,
35 protocol::mux::{
36 BackendChange, BackendStatus, Context, DebugEvent, DebugHistory, Endpoint,
37 GenericHttpStream, GlobalStreamId, MuxResult, Position, Stream, StreamId, StreamState,
38 converter, forcefully_terminate_answer,
39 h2_close::{self, CloseAction, FinalizeAction, TlsFlushPhase},
40 h2_control_tx,
41 h2_drain::{self, GracefulDrainDecision},
42 h2_flood_detector::{self, H2FloodConfig, H2FloodViolation},
43 h2_flow_control, h2_header_reassembly, h2_output, h2_scheduler, h2_stream_table,
44 h2_transmit,
45 h2_write_pass::{H2WritePass, H2WritePhase},
46 hpack_state,
47 parser::{self, Frame, FrameHeader, FrameType, H2Error, Headers, WindowUpdate},
48 pkawa, remove_backend_stream, serializer, set_default_answer,
49 shared::{
50 ERROR_GOAWAY_LINGER_MAX_BYTES, ERROR_GOAWAY_LINGER_TIMEOUT, EndStreamAction,
51 LINGER_MAX_BYTES, Linger, LingerRead, drain_discard, drain_tls_close_notify,
52 end_stream_decision,
53 },
54 update_readiness_after_read, update_readiness_after_write,
55 },
56 socket::{SocketHandler, SocketResult},
57};
58
59/// Protocol label + session descriptor used as a prefix on every
60/// [`ConnectionH2`] log line. Matches the RUSTLS log-context convention:
61/// `MUX-H2\tSession(...)\t >>>`. When colored output is enabled (via
62/// [`ansi_palette`]) the label is wrapped in bold bright-white ANSI (uniform
63/// across every protocol) and the session detail is rendered in light grey.
64///
65/// Fields included in the session block (chosen to surface the most common
66/// H2 troubleshooting axes — flow stall, leaked stream, draining state,
67/// peer-side gap, reset-flood exposure):
68/// - `peer` — peer address via [`SocketHandler::peer_addr`](crate::socket::SocketHandler::peer_addr):
69/// the snapshot the handler cached, not a live `getpeername(2)`. It therefore
70/// survives the peer's RST (which is when these lines are read) and, on a
71/// PROXY-protocol frontend, names the advertised client rather than the load
72/// balancer — matching the `HTTPS`/`HTTP` line for the same request id
73/// - `position` — `Server` / `Client(...)` orientation
74/// - `state` — current [`H2State`]
75/// - `streams` — number of in-flight streams on this connection
76/// - `last_peer_id` — `highest_peer_stream_id` (gap to the peer's view)
77/// - `window` — connection-level send window (RFC 9113 §6.9)
78/// - `draining` — set after the first GOAWAY of a graceful shutdown
79/// - `total_rst_streams_emitted_lifetime` — MadeYouReset counter (CVE-2025-8671)
80/// - `total_rst_received_lifetime` — Rapid Reset counter (CVE-2023-44487)
81/// - `readiness` — connection-level mio readiness snapshot
82///
83/// Computed lazily on each callsite — the helper only materialises when the
84/// log level is enabled, so uncolored hot paths keep a single thread-local
85/// read (the colored check) and one `format!` allocation.
86macro_rules! log_context {
87 ($self:expr) => {{
88 let (open, reset, grey, gray, white) = ansi_palette();
89 format!(
90 "[{ulid} - - -]\t{open}MUX-H2{reset}\t{grey}Session{reset}({gray}peer{reset}={white}{peer:?}{reset}, {gray}position{reset}={white}{position:?}{reset}, {gray}state{reset}={white}{state:?}{reset}, {gray}streams{reset}={white}{streams}{reset}, {gray}last_peer_id{reset}={white}{last_peer_id}{reset}, {gray}window{reset}={white}{window}{reset}, {gray}draining{reset}={white}{draining}{reset}, {gray}total_rst_streams_emitted_lifetime{reset}={white}{total_rst_streams_emitted_lifetime}{reset}, {gray}total_rst_received_lifetime{reset}={white}{total_rst_received_lifetime}{reset}, {gray}readiness{reset}={white}{readiness}{reset})\t >>>",
91 open = open,
92 reset = reset,
93 grey = grey,
94 gray = gray,
95 white = white,
96 ulid = $self.session_ulid,
97 peer = $self.peer_address,
98 position = $self.position,
99 state = $self.state,
100 streams = $self.stream_table.len(),
101 last_peer_id = $self.stream_table.highest_peer_stream_id(),
102 window = $self.flow_control.window(),
103 draining = $self.drain.draining(),
104 total_rst_streams_emitted_lifetime = $self.flood_detector.total_rst_streams_emitted_lifetime(),
105 total_rst_received_lifetime = $self.flood_detector.total_rst_received_lifetime(),
106 readiness = $self.readiness,
107 )
108 }};
109}
110
111/// Per-stream variant of [`log_context!`] used when a [`Stream`]'s
112/// [`HttpContext`](crate::protocol::kawa_h1::editor::HttpContext) is in
113/// scope. Populates the `request_id`, `cluster_id` and `backend_id` slots of
114/// the bracket so the log line can be filtered by the specific H2 stream it
115/// belongs to.
116#[allow(unused_macros)]
117macro_rules! log_context_stream {
118 ($self:expr, $http_context:expr) => {{
119 let (open, reset, grey, gray, white) = ansi_palette();
120 format!(
121 "[{ulid} {req} {cluster} {backend}]\t{open}MUX-H2{reset}\t{grey}Session{reset}({gray}peer{reset}={white}{peer:?}{reset}, {gray}position{reset}={white}{position:?}{reset}, {gray}state{reset}={white}{state:?}{reset}, {gray}streams{reset}={white}{streams}{reset}, {gray}last_peer_id{reset}={white}{last_peer_id}{reset}, {gray}window{reset}={white}{window}{reset}, {gray}draining{reset}={white}{draining}{reset}, {gray}total_rst_streams_emitted_lifetime{reset}={white}{total_rst_streams_emitted_lifetime}{reset}, {gray}total_rst_received_lifetime{reset}={white}{total_rst_received_lifetime}{reset}, {gray}readiness{reset}={white}{readiness}{reset})\t >>>",
122 open = open,
123 reset = reset,
124 grey = grey,
125 gray = gray,
126 white = white,
127 ulid = $self.session_ulid,
128 req = $http_context.id,
129 cluster = $http_context.cluster_id.as_deref().unwrap_or("-"),
130 backend = $http_context.backend_id.as_deref().unwrap_or("-"),
131 peer = $self.peer_address,
132 position = $self.position,
133 state = $self.state,
134 streams = $self.stream_table.len(),
135 last_peer_id = $self.stream_table.highest_peer_stream_id(),
136 window = $self.flow_control.window(),
137 draining = $self.drain.draining(),
138 total_rst_streams_emitted_lifetime = $self.flood_detector.total_rst_streams_emitted_lifetime(),
139 total_rst_received_lifetime = $self.flood_detector.total_rst_received_lifetime(),
140 readiness = $self.readiness,
141 )
142 }};
143}
144
145/// Module-level prefix without session context, for logs emitted from
146/// free functions, `H2ConnectionConfig` validation and other sites where no
147/// `ConnectionH2` is in scope. Keeps the `MUX-H2` label consistent with
148/// connection logs and honours the colored flag.
149macro_rules! log_module_context {
150 () => {{
151 let (open, reset, _, _, _) = ansi_palette();
152 format!("{open}MUX-H2{reset}\t >>>", open = open, reset = reset)
153 }};
154}
155
156/// `if let Some(violation) = self.flood_detector.check_flood(self.now) { return self.handle_flood_violation(violation); }`
157/// pattern wrapped as a single statement. Pure dispatch — the actual flood
158/// thresholds and counters live inside `H2FloodDetector::check_flood` and
159/// `ConnectionH2::handle_flood_violation`, which the macro does not touch.
160/// Use this at every per-frame counter bump site so the wrapper stays
161/// uniform and a future grep for "flood-check forgot to return" finds zero.
162macro_rules! check_flood_or_return {
163 ($self:expr) => {
164 if let Some(violation) = $self.flood_detector.check_flood($self.now) {
165 return $self.handle_flood_violation(violation);
166 }
167 };
168}
169
170// ── RFC 9113 §6.5.2 Settings Defaults ───────────────────────────────────────
171
172const DEFAULT_HEADER_TABLE_SIZE: u32 = 4096;
173const DEFAULT_MAX_CONCURRENT_STREAMS: u32 = 100;
174pub(super) const DEFAULT_INITIAL_WINDOW_SIZE: u32 = (1 << 16) - 1; // 65535
175const DEFAULT_MAX_FRAME_SIZE: u32 = 1 << 14; // 16384
176
177// RFC 9113 §6.5.2: SETTINGS_MAX_FRAME_SIZE valid range [2^14, 2^24)
178const MIN_MAX_FRAME_SIZE: u32 = 1 << 14; // 16384
179const MAX_MAX_FRAME_SIZE: u32 = 1 << 24; // 16777216 (exclusive upper bound)
180
181// RFC 9113 §6.9: maximum flow control window size (2^31 - 1)
182const FLOW_CONTROL_MAX_WINDOW: u32 = (1 << 31) - 1;
183// RFC 9113 §5.1.1: stream identifiers are 31-bit unsigned integers (2^31 - 1).
184const STREAM_ID_MAX: u32 = 0x7FFF_FFFF;
185
186/// Allocate the next locally-initiated stream identifier given the current
187/// `last_stream_id` watermark, returning `(issued_id, next_last_stream_id)`
188/// or `None` when the 31-bit space is exhausted.
189///
190/// RFC 9113 §5.1.1 reserves odd identifiers for clients and even identifiers
191/// for servers. Sōzu never server-pushes, so in practice this helper is
192/// called on the backend (client) side via [`ConnectionH2::new_stream_id`].
193/// The server branch is kept symmetrical so the behaviour is exercised by
194/// the unit tests and remains correct if push is ever enabled.
195///
196/// `last_stream_id` tracks the even "watermark" (2, 4, 6, ...). A client call
197/// issues `watermark - 1` (odd), a server call issues `watermark - 2` (even).
198/// The helper enforces two invariants:
199/// - the issued identifier never exceeds `STREAM_ID_MAX` (2³¹ - 1); and
200/// - the returned watermark is a valid starting point for the next call.
201///
202/// Exhaustion is reported with `None` to the caller, which must emit
203/// GOAWAY(NO_ERROR) and stop issuing new streams on this connection
204/// (see `start_stream` for the client-side drain path).
205pub(super) fn next_stream_id(
206 last_stream_id: StreamId,
207 is_client: bool,
208) -> Option<(StreamId, StreamId)> {
209 let next = last_stream_id.checked_add(2)?;
210 let issued = if is_client {
211 next.checked_sub(1)?
212 } else {
213 next.checked_sub(2)?
214 };
215 // RFC 9113 §5.1.1: stream identifiers are 31-bit. Reject any allocation
216 // whose issued value would exceed `STREAM_ID_MAX`; the watermark itself
217 // is allowed to sit at `STREAM_ID_MAX + 1` (the sentinel that fails the
218 // next call).
219 if issued > STREAM_ID_MAX {
220 return None;
221 }
222 // Post-conditions (RFC 9113 §5.1.1):
223 // - the issued id fits the 31-bit space;
224 // - the returned watermark is strictly greater than the id we issued, so a
225 // subsequent call cannot re-issue or regress;
226 // - role-parity: client ids are odd, server ids even. This holds ONLY when
227 // `last_stream_id` is an even watermark, which is the helper's documented
228 // contract and what production always maintains (`create_stream` rounds to
229 // `(stream_id + 2) & !1`; the connection initialises it to 0). The unit
230 // tests deliberately feed odd `last` values at the saturation boundary, so
231 // the parity check is gated on the watermark being even — a parity slip
232 // from an *even* watermark would let two roles collide on one id.
233 debug_assert!(
234 issued <= STREAM_ID_MAX,
235 "issued stream id must fit the 31-bit space"
236 );
237 debug_assert!(
238 next > issued,
239 "the next watermark must advance strictly past the issued id"
240 );
241 debug_assert!(
242 last_stream_id & 1 != 0 || (issued & 1 == 1) == is_client,
243 "from an even watermark, client ids must be odd and server ids even (RFC 9113 §5.1.1)"
244 );
245 Some((issued, next))
246}
247
248/// Connection receive window (16 MiB), advertised and not enforced (see
249/// `lib/src/protocol/mux/h2_flow_control.rs`). Credit returns in one stream-0 WINDOW_UPDATE per
250/// half window received, one per 8 MiB, well under peers' stream-0 flood limits (Chromium uses
251/// 15 MB, Envoy 24 MiB). The buffer pool bounds memory: DATA is read only when its buffer has room.
252const ENLARGED_CONNECTION_WINDOW: u32 = 16 * 1024 * 1024;
253
254/// H2 client connection preface size: 24-byte magic + 9-byte SETTINGS frame header
255pub const CLIENT_PREFACE_SIZE: usize = 24 + parser::FRAME_HEADER_SIZE;
256
257// ── Flood Detection Thresholds (CVE mitigations) ────────────────────────────
258//
259// The CVE-tagged threshold defaults, `H2FloodConfig`, `H2FloodViolation` and
260// `H2FloodDetector` itself moved into `h2_flood_detector.rs`, behind the same
261// closed-API shape `hpack_state.rs`, `h2_flow_control.rs` and
262// `h2_stream_table.rs` established in the three prior extraction steps.
263// `MAX_HEADER_LIST_SIZE` stays here: `converter.rs` and `pkawa.rs` reference
264// it directly (`h2::MAX_HEADER_LIST_SIZE`) as a general HPACK encode/decode
265// safety ceiling, independent of any one connection's configured
266// `H2FloodConfig::max_header_list_size` — moving it would widen this
267// extraction into two unrelated modules for no benefit.
268
269/// Maximum accumulated header block size across CONTINUATION frames (64KB).
270/// Also the compile-time default for `H2FloodConfig::max_header_list_size`
271/// (`h2_flood_detector.rs`).
272pub(super) const MAX_HEADER_LIST_SIZE: usize = 65536;
273/// Cumulative outbound progress (bytes) a window-stalled stream must drain to
274/// clear its flow-control-stall deadline (M2 cumulative-stall budget). Below
275/// this, a `WINDOW_UPDATE(+1)` drip that trickles a few bytes per idle period
276/// cannot keep the slot alive: the deadline ages out and the reaper
277/// RST(CANCEL)s the stream. Chosen as one max H2 DATA frame payload (16 KiB) —
278/// a legitimate slow-but-steady transfer drains at least one frame per idle
279/// period at any realistic bandwidth, while a drip attacker grants far less. A
280/// `const`, not a config knob: `h2_stream_idle_timeout_seconds` is already the
281/// operator dial for slow-link tolerance, and coupling a second knob invites
282/// misconfiguration (high floor + low deadline = mass false reaps).
283const FC_STALL_CLEAR_FLOOR: usize = 16 * 1024;
284/// RFC 9113 §6.5.2: the size accounted against `SETTINGS_MAX_HEADER_LIST_SIZE`
285/// is the uncompressed name + value octets PLUS a 32-octet overhead per field.
286/// The per-field overhead is what bounds the field count under a fixed byte
287/// budget — omitting it lets a peer materialize ~33× more fields than intended.
288pub(super) const HEADER_FIELD_SIZE_OVERHEAD: usize = 32;
289
290/// RFC 9113 §5.1.2: threshold of `REFUSED_STREAM` emissions per
291/// [`BACKPRESSURE_WINDOW_DURATION`] that triggers back-pressure — at this
292/// point we halve the advertised `SETTINGS_MAX_CONCURRENT_STREAMS` so the
293/// peer throttles its request rate instead of paying the RST round-trip for
294/// every new stream.
295const BACKPRESSURE_REFUSAL_THRESHOLD: u32 = 1000;
296/// Sliding window used to detect refusal bursts for SETTINGS back-pressure.
297const BACKPRESSURE_WINDOW_DURATION: std::time::Duration = std::time::Duration::from_secs(60);
298
299/// Default stream Vec shrink ratio: shrink when total > active * ratio.
300const DEFAULT_STREAM_SHRINK_RATIO: u32 = 2;
301
302/// Configurable H2 connection tuning parameters.
303///
304/// All values have safe defaults. When configured via listener config,
305/// absent values fall back to compile-time defaults.
306#[derive(Debug, Clone, Copy, PartialEq, Eq)]
307pub struct H2ConnectionConfig {
308 /// Connection-level receive window size in bytes (RFC 9113 §6.9.2). Advertised, not enforced — see `h2_flow_control`.
309 pub initial_connection_window: u32,
310 /// Maximum concurrent streams (SETTINGS_MAX_CONCURRENT_STREAMS).
311 pub max_concurrent_streams: u32,
312 /// Shrink threshold ratio for recycled stream slots.
313 pub stream_shrink_ratio: u32,
314}
315
316impl Default for H2ConnectionConfig {
317 fn default() -> Self {
318 Self {
319 initial_connection_window: ENLARGED_CONNECTION_WINDOW,
320 max_concurrent_streams: DEFAULT_MAX_CONCURRENT_STREAMS,
321 stream_shrink_ratio: DEFAULT_STREAM_SHRINK_RATIO,
322 }
323 }
324}
325
326impl H2ConnectionConfig {
327 /// Create a validated config, clamping to safe bounds.
328 ///
329 /// - `initial_connection_window`: clamped to \[65535, 2^31-1\] per RFC 9113 §6.9
330 /// - `max_concurrent_streams`: minimum 1
331 /// - `stream_shrink_ratio`: minimum 2 (1 would defeat slot recycling)
332 pub fn new(
333 initial_connection_window: u32,
334 max_concurrent_streams: u32,
335 stream_shrink_ratio: u32,
336 ) -> Self {
337 let clamped_window =
338 initial_connection_window.clamp(DEFAULT_INITIAL_WINDOW_SIZE, FLOW_CONTROL_MAX_WINDOW);
339 if clamped_window != initial_connection_window {
340 warn!(
341 "{} h2_initial_connection_window {} clamped to [{}, {}]",
342 log_module_context!(),
343 initial_connection_window,
344 DEFAULT_INITIAL_WINDOW_SIZE,
345 FLOW_CONTROL_MAX_WINDOW
346 );
347 }
348 const MAX_SAFE_CONCURRENT_STREAMS: u32 = 10_000;
349 let clamped_streams = max_concurrent_streams.clamp(1, MAX_SAFE_CONCURRENT_STREAMS);
350 if max_concurrent_streams > MAX_SAFE_CONCURRENT_STREAMS {
351 error!(
352 "{} h2_max_concurrent_streams={} exceeds safe limit, clamped to {}",
353 log_module_context!(),
354 max_concurrent_streams,
355 MAX_SAFE_CONCURRENT_STREAMS
356 );
357 }
358 if clamped_streams != max_concurrent_streams
359 && max_concurrent_streams <= MAX_SAFE_CONCURRENT_STREAMS
360 {
361 warn!(
362 "{} h2_max_concurrent_streams {} clamped to minimum 1",
363 log_module_context!(),
364 max_concurrent_streams
365 );
366 }
367 let clamped_ratio = stream_shrink_ratio.max(2);
368 if clamped_ratio != stream_shrink_ratio {
369 warn!(
370 "{} h2_stream_shrink_ratio {} clamped to minimum 2",
371 log_module_context!(),
372 stream_shrink_ratio
373 );
374 }
375 let config = Self {
376 initial_connection_window: clamped_window,
377 max_concurrent_streams: clamped_streams,
378 stream_shrink_ratio: clamped_ratio,
379 };
380 // Post-conditions matching the documented clamp ranges. The window must
381 // stay within RFC 9113 §6.9's [65535, 2^31-1] (a window outside this
382 // band desynchronises flow control with the peer); max_concurrent_streams
383 // must be >= 1 (zero would refuse every stream); shrink_ratio must be
384 // >= 2 (1 defeats slot recycling, the whole point of the knob).
385 debug_assert!(
386 (DEFAULT_INITIAL_WINDOW_SIZE..=FLOW_CONTROL_MAX_WINDOW)
387 .contains(&config.initial_connection_window),
388 "clamped connection window must lie within RFC 9113 §6.9 bounds"
389 );
390 debug_assert!(
391 config.max_concurrent_streams >= 1,
392 "clamped max_concurrent_streams must be >= 1"
393 );
394 debug_assert!(
395 config.stream_shrink_ratio >= 2,
396 "clamped stream_shrink_ratio must be >= 2 to keep slot recycling effective"
397 );
398 config
399 }
400
401 /// Create from optional config values, falling back to compile-time defaults.
402 /// Combines unwrap-or-default with validation clamping.
403 pub fn from_optional(
404 window: Option<u32>,
405 max_streams: Option<u32>,
406 shrink_ratio: Option<u32>,
407 ) -> Self {
408 let defaults = Self::default();
409 Self::new(
410 window.unwrap_or(defaults.initial_connection_window),
411 max_streams.unwrap_or(defaults.max_concurrent_streams),
412 shrink_ratio.unwrap_or(defaults.stream_shrink_ratio),
413 )
414 }
415}
416
417/// Default pending WINDOW_UPDATE capacity (used in tests).
418/// The actual per-connection cap is computed from `connection_config.max_concurrent_streams`.
419#[cfg(test)]
420const DEFAULT_MAX_PENDING_WINDOW_UPDATES: usize = 1 + DEFAULT_MAX_CONCURRENT_STREAMS as usize * 4;
421
422/// RFC 9113 §6.5: maximum time (in seconds) to wait for SETTINGS ACK before
423/// sending GOAWAY with SETTINGS_TIMEOUT error code.
424const SETTINGS_ACK_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
425
426/// Wire size of the SETTINGS frame `serializer::gen_settings` writes.
427const SETTINGS_FRAME_SIZE: usize =
428 parser::FRAME_HEADER_SIZE + (parser::SETTINGS_ENTRY_SIZE * parser::SETTINGS_COUNT) as usize;
429/// Wire size of one WINDOW_UPDATE frame.
430const WINDOW_UPDATE_FRAME_SIZE: usize =
431 parser::FRAME_HEADER_SIZE + parser::WINDOW_UPDATE_PAYLOAD_SIZE as usize;
432/// Wire size of one RST_STREAM frame.
433const RST_STREAM_FRAME_SIZE: usize =
434 parser::FRAME_HEADER_SIZE + parser::RST_STREAM_PAYLOAD_SIZE as usize;
435/// Wire size of one GOAWAY frame without debug data.
436const GOAWAY_FRAME_SIZE: usize = parser::FRAME_HEADER_SIZE + parser::GOAWAY_PAYLOAD_SIZE as usize;
437
438#[inline(always)]
439fn error_nom_to_h2(error: nom::Err<parser::ParserError>) -> H2Error {
440 match error {
441 nom::Err::Error(parser::ParserError {
442 kind: parser::ParserErrorKind::H2(e),
443 ..
444 }) => e,
445 nom::Err::Failure(parser::ParserError {
446 kind: parser::ParserErrorKind::H2(e),
447 ..
448 }) => e,
449 _ => H2Error::ProtocolError,
450 }
451}
452
453/// Distribute connection-level byte overhead proportionally to a single stream.
454///
455/// Overhead is distributed in proportion to the bytes this stream transferred
456/// relative to the total across all active streams. A stream that transferred
457/// 60% of total bytes gets 60% of the overhead.
458///
459/// `stream_bytes` and `total_bytes` are `(bytes_in, bytes_out)` tuples.
460/// Falls back to even distribution (1/active_streams) when no stream has
461/// transferred any bytes yet (total is zero).
462///
463/// A free function rather than a method: the seven `test_distribute_overhead_*`
464/// cases drive this arithmetic directly, with no `ConnectionH2` fixture, the
465/// same reason [`any_stream_id_matches`] is split out.
466/// [`ConnectionH2::distribute_overhead`] is the `&mut self` wrapper the reset
467/// paths use. [`ConnectionH2::try_recycle_server_stream`] calls this function
468/// directly instead, which is a spelling choice and not a constraint: the
469/// wrapper would credit the same shares at that site, because it reads
470/// `self.stream_table.streams().len()` at call time — which is the same count,
471/// the pass retiring nothing until after its loop — and its `len() <= 1`
472/// cannot differ from the `len() == 1` spelled out there while the stream
473/// being retired is still in the map.
474fn distribute_overhead(
475 metrics: &mut SessionMetrics,
476 overhead_bin: &mut usize,
477 overhead_bout: &mut usize,
478 stream_bytes: (usize, usize),
479 total_bytes: (usize, usize),
480 active_streams: usize,
481 is_last_stream: bool,
482) {
483 let share_in = if is_last_stream {
484 // Last stream gets all remaining overhead to avoid losing remainder bytes
485 // from integer division across earlier streams.
486 *overhead_bin
487 } else if let Some(share) = (*overhead_bin * stream_bytes.0).checked_div(total_bytes.0) {
488 // Clamp to remaining overhead — integer division rounding across multiple
489 // streams can cause accumulated shares to exceed the total.
490 share.min(*overhead_bin)
491 } else {
492 // No stream has transferred any inbound bytes — fall back to even split.
493 *overhead_bin / active_streams.max(1)
494 };
495 let share_out = if is_last_stream {
496 *overhead_bout
497 } else if let Some(share) = (*overhead_bout * stream_bytes.1).checked_div(total_bytes.1) {
498 share.min(*overhead_bout)
499 } else {
500 // No stream has transferred any outbound bytes — fall back to even split.
501 *overhead_bout / active_streams.max(1)
502 };
503 // Pre-condition: a stream can never be credited more overhead than remains
504 // in the pool — otherwise the `*overhead_b* -= share_*` below underflows
505 // (usize wraps to a huge value, corrupting connection-overhead accounting).
506 // Every branch above either takes the whole pool (last stream) or `.min`s
507 // against it, so this must hold.
508 debug_assert!(
509 share_in <= *overhead_bin,
510 "overhead-in share must not exceed the remaining overhead pool"
511 );
512 debug_assert!(
513 share_out <= *overhead_bout,
514 "overhead-out share must not exceed the remaining overhead pool"
515 );
516 let before_bin = *overhead_bin;
517 let before_bout = *overhead_bout;
518 metrics.bin += share_in;
519 metrics.bout += share_out;
520 *overhead_bin -= share_in;
521 *overhead_bout -= share_out;
522 // Post-condition: the pool shrinks by exactly the credited share (overhead
523 // is conserved, neither created nor lost). The last stream drains it to 0.
524 debug_assert_eq!(
525 *overhead_bin,
526 before_bin - share_in,
527 "overhead-in pool must decrease by exactly the credited share"
528 );
529 debug_assert_eq!(
530 *overhead_bout,
531 before_bout - share_out,
532 "overhead-out pool must decrease by exactly the credited share"
533 );
534 debug_assert!(
535 !is_last_stream || (*overhead_bin == 0 && *overhead_bout == 0),
536 "the last stream must drain the overhead pool to zero (no lost remainder)"
537 );
538}
539
540/// LIFECYCLE §9 invariant 16 probe: returns `true` if any open stream still
541/// has outbound kawa bytes queued (`back.out` non-empty or `back.blocks`
542/// non-drained).
543///
544/// Used by `finalize_write` to preserve `Ready::WRITABLE` across a voluntary
545/// scheduler yield, and by `has_pending_write_full` to block shutdown-drain
546/// while bytes are still owed to the frontend.
547///
548/// `.get()` rather than direct indexing: an unknown `GlobalStreamId` is
549/// treated as "no pending bytes" rather than panicking — defence-in-depth
550/// against a stream-removal race during shutdown.
551fn any_stream_has_pending_back(
552 streams: &BTreeMap<StreamId, GlobalStreamId>,
553 context_streams: &[Stream],
554) -> bool {
555 any_stream_id_matches(streams, |gid| {
556 context_streams
557 .get(gid)
558 .is_some_and(|s| !s.back.out.is_empty() || !s.back.blocks.is_empty())
559 })
560}
561
562/// Iteration core of [`any_stream_has_pending_back`], split out so the
563/// invariant-16 dispatch is unit-testable without a full [`Stream`] fixture
564/// (the existing test module only covers `H2FloodDetector`).
565fn any_stream_id_matches<F>(streams: &BTreeMap<StreamId, GlobalStreamId>, mut probe: F) -> bool
566where
567 F: FnMut(GlobalStreamId) -> bool,
568{
569 streams.values().any(|gid| probe(*gid))
570}
571
572/// The send credit `kawa.out` still holds in whole, unsent DATA frames
573/// ([`h2_transmit::queued_data_payload`]), as the `i32` both send windows
574/// count in. Every one of those octets was debited from a window that never
575/// exceeds 2^31-1, so the conversion cannot fail; if it ever did, answering
576/// zero gives back too little rather than too much.
577fn parked_data_credit(kawa: &GenericHttpStream) -> i32 {
578 let queued = h2_transmit::queued_data_payload(kawa);
579 let credit = i32::try_from(queued).unwrap_or(0);
580 debug_assert_eq!(
581 usize::try_from(credit).ok(),
582 Some(queued),
583 "queued DATA fits the window it was debited from"
584 );
585 credit
586}
587
588/// True when a stream still has response/upload bytes that could be put on the
589/// wire — headers/body in flight, or a terminated-but-not-fully-flushed buffer.
590/// Deliberately EXCLUDES `is_error()`/`rst_sent`: that disjunct is specific to
591/// the priority-eligibility and write-loop gates (`write_streams`) and must stay
592/// inline there; this 2-clause helper backs ONLY the window-stall arm.
593fn has_sendable_response(kawa: &GenericHttpStream) -> bool {
594 kawa.is_main_phase() || (kawa.is_terminated() && !kawa.is_completed())
595}
596
597/// Outcome of the M2 cumulative-stall budget decision for one `write_streams`
598/// pass on a window-stalled stream. Extracted from the `write_streams` arm so
599/// the budget logic is unit-testable without a full `ConnectionH2` fixture
600/// (mirrors the [`h2_stream_table::H2StreamTable::collect_timed_out`] extraction).
601#[derive(Debug, Clone, Copy, PartialEq, Eq)]
602enum FcStallAction {
603 /// Clear both the deadline (`stream_fc_stalled_since`) and the progress
604 /// accumulator (`stream_fc_stalled_progress`) for this stream.
605 Clear,
606 /// Ensure the deadline is armed (WITHOUT refreshing an existing `Instant`)
607 /// and set the progress accumulator to `progress`.
608 Arm { progress: usize },
609}
610
611/// Decide what to do with a stream's flow-control-stall deadline + cumulative
612/// progress accumulator on one write pass (M2 cumulative-stall budget).
613///
614/// - A genuinely open send window (`!outbound_window_blocked`) is a real
615/// un-stall → [`FcStallAction::Clear`].
616/// - While the window stays blocked, accumulate this pass's outbound drain
617/// (`consumed`, clamped to `>= 0`) onto `prior_progress`. Once the cumulative
618/// total reaches [`FC_STALL_CLEAR_FLOOR`] (a full DATA frame of real delivery)
619/// → `Clear`; otherwise `Arm` with the running total. A `WINDOW_UPDATE(+1)`
620/// drip adds ~1 byte/pass and never reaches the floor, so the deadline keeps
621/// aging and the reaper eventually fires.
622fn fc_stall_budget_decision(
623 outbound_window_blocked: bool,
624 consumed: i32,
625 prior_progress: Option<usize>,
626) -> FcStallAction {
627 if !outbound_window_blocked {
628 return FcStallAction::Clear;
629 }
630 let progressed = prior_progress
631 .unwrap_or(0)
632 .saturating_add(consumed.max(0) as usize);
633 if progressed >= FC_STALL_CLEAR_FLOOR {
634 FcStallAction::Clear
635 } else {
636 FcStallAction::Arm {
637 progress: progressed,
638 }
639 }
640}
641
642#[derive(Debug)]
643pub enum H2State {
644 ClientPreface,
645 ClientSettings,
646 ServerSettings,
647 Header,
648 Frame(FrameHeader),
649 ContinuationHeader(Headers),
650 ContinuationFrame(Headers),
651 GoAway,
652 Error,
653 Discard,
654}
655
656#[derive(Debug, Clone, Copy)]
657pub struct H2Settings {
658 pub settings_header_table_size: u32,
659 pub settings_enable_push: bool,
660 pub settings_max_concurrent_streams: u32,
661 pub settings_initial_window_size: u32,
662 pub settings_max_frame_size: u32,
663 pub settings_max_header_list_size: u32,
664 /// RFC 8441
665 pub settings_enable_connect_protocol: bool,
666 /// RFC 9218
667 pub settings_no_rfc7540_priorities: bool,
668}
669
670impl Default for H2Settings {
671 fn default() -> Self {
672 Self {
673 settings_header_table_size: DEFAULT_HEADER_TABLE_SIZE,
674 settings_enable_push: false,
675 settings_max_concurrent_streams: DEFAULT_MAX_CONCURRENT_STREAMS,
676 settings_initial_window_size: DEFAULT_INITIAL_WINDOW_SIZE,
677 settings_max_frame_size: DEFAULT_MAX_FRAME_SIZE,
678 settings_max_header_list_size: MAX_HEADER_LIST_SIZE as u32,
679 settings_enable_connect_protocol: false,
680 settings_no_rfc7540_priorities: true,
681 }
682 }
683}
684
685/// Byte accounting for connection overhead attribution.
686pub struct H2ByteAccounting {
687 /// Bytes read on the zero stream not yet attributed to a stream.
688 pub zero_bytes_read: usize,
689 /// Overhead bytes received (connection-level frames).
690 pub overhead_bin: usize,
691 /// Overhead bytes sent (connection-level frames).
692 pub overhead_bout: usize,
693}
694
695/// What `ConnectionH2::handle_data_frame` does with the payload of a DATA
696/// frame of a backend response (`ConnectionH2::backend_response_content`).
697#[derive(Debug, Clone, Copy, PartialEq, Eq)]
698enum BackendResponseContent {
699 /// The response may carry content: the payload is forwarded.
700 Forwarded,
701 /// A response to HEAD, which SHOULD NOT carry content (RFC 9110
702 /// §9.3.2): the payload is discarded.
703 Discarded,
704 /// A 204 or a 304, which cannot carry content (RFC 9110 §15.3.5,
705 /// §15.4.5): a payload resets the backend stream.
706 Forbidden,
707}
708
709pub struct ConnectionH2 {
710 /// Connection/session ULID propagated from the parent [`super::Mux`]. Used to
711 /// stamp the session slot of the `[session req cluster backend]` log
712 /// prefix emitted by this module's `log_context!` / `log_context_stream!`
713 /// macros.
714 pub session_ulid: Ulid,
715 /// HPACK decoder/encoder pair and their reusable scratch buffers
716 /// (`converter_buf`, `lowercase_buf`, `cookie_buf`), encapsulated so
717 /// nothing outside `hpack_state.rs` can reach the raw fields — see
718 /// [`hpack_state::HpackState`]. The pass-ordering buffer that used to sit
719 /// beside them belongs to [`h2_scheduler::H2Scheduler`].
720 hpack: hpack_state::HpackState,
721 pub last_stream_id: StreamId,
722 pub local_settings: H2Settings,
723 pub peer_settings: H2Settings,
724 pub position: Position,
725 /// RFC 9218 priority state and the per-write-pass scheduling decision
726 /// (stream order, the same-urgency ready-incremental census, the
727 /// round-robin cursor), encapsulated so nothing outside
728 /// `h2_scheduler.rs` can reach the raw fields — see
729 /// [`h2_scheduler::H2Scheduler`].
730 scheduler: h2_scheduler::H2Scheduler,
731 pub readiness: Readiness,
732 /// Peer address of this connection, captured once at construction from
733 /// [`SocketHandler::peer_addr`](crate::socket::SocketHandler::peer_addr)
734 /// and never re-read.
735 ///
736 /// This is the `peer=` slot of every line `log_context!` /
737 /// `log_context_stream!` render, and those two macros expand at 113
738 /// production callsites in this file — so sourcing the slot from
739 /// `self.socket` made *every logging method* socket-coupled, including
740 /// the ones that touch no I/O at all.
741 ///
742 /// Both production handlers *prefer* a cached address and fall back to
743 /// a live `getpeername(2)` when they have none — `SessionTcpStream` and
744 /// `FrontRustls` each answer
745 /// `self.configured_peer.or_else(|| self.stream.peer_addr().ok())`
746 /// (`socket.rs`). **That fallback arm is reachable**, it is pinned by its
747 /// own tests, and `FrontRustls`' arm carries a comment saying so and
748 /// naming the test that fails without it. So this snapshot is not
749 /// unconditionally the same value a per-line call would have produced,
750 /// and nothing here may be read as licence to delete those arms.
751 ///
752 /// It is at least as good on every path, which is the actual argument.
753 /// The snapshot is taken after the handshake, so wherever the fallback
754 /// would have answered, it answers here too — and it then survives the
755 /// peer's reset, where a later live lookup returns `ENOTCONN` and renders
756 /// `peer=None` on exactly the error lines an operator reads during an
757 /// incident. On the direct-HTTPS frontend route, where `HttpsSession::new`
758 /// seeds `peer_address` with a best-effort `peer_addr().ok()`, a line
759 /// emitted after a reset now renders `peer=Some(addr)` where it used to
760 /// render `peer=None`. That is a rendered-line change, and it is
761 /// `df52d83d`'s intent applied one level further in.
762 ///
763 /// No production caller reaches [`SocketHandler::peer_addr`] on a bare
764 /// `mio::net::TcpStream`. The impl exists and `MioTcpStream` is
765 /// instantiated in production (`TcpStateMachine`, `tcp.rs`), but the TCP
766 /// proxy reads its peer through mio's inherent method; only tests call
767 /// the trait method on a bare stream.
768 pub(super) peer_address: Option<std::net::SocketAddr>,
769 pub state: H2State,
770 /// Wire `StreamId -> GlobalStreamId` map, `expect_read`/`expect_write`,
771 /// `highest_peer_stream_id`, `rst_sent`, and the per-stream
772 /// activity/flow-control-stall caches, encapsulated so nothing outside
773 /// `h2_stream_table.rs` can reach the raw fields — see
774 /// [`h2_stream_table::H2StreamTable`].
775 stream_table: h2_stream_table::H2StreamTable,
776 /// Configured idle timeout for this connection. The core never arms a
777 /// wheel entry itself: it publishes the next instant it wants to be called
778 /// back at through `ConnectionH2::poll_timeout`, and the embedder — the
779 /// `Mux` adapter — owns the `TimeoutContainer` that reflects it onto the
780 /// real timer. See `LIFECYCLE.md` §7.7.
781 pub timeout_duration: Duration,
782 /// Next instant this connection wants `timeout()` called at, or `None`
783 /// when it wants no timer at all. Always derived from
784 /// [`ConnectionH2::now`], never from a fresh clock read (invariant 20).
785 pub(super) timeout_deadline: Option<Instant>,
786 /// Connection-level flow control state (send window, receive tracking,
787 /// pending updates), encapsulated so nothing outside `h2_flow_control.rs`
788 /// can reach the raw fields — see [`h2_flow_control::H2FlowControl`].
789 flow_control: h2_flow_control::H2FlowControl,
790 /// RFC 7541 §4.2 / §6.3 pending dynamic-table-size-update signal.
791 ///
792 /// `Some(new_size)` when a peer SETTINGS frame adjusted
793 /// `SETTINGS_HEADER_TABLE_SIZE` and we have not yet prepended the
794 /// matching `001xxxxx` HPACK directive to a header block. Consumed and
795 /// cleared by `H2BlockConverter::emit_pending_size_update_if_new_block`
796 /// on the next `Block::StatusLine` or `Block::Header` encoded for the
797 /// connection. Until then the peer's decoder still has its previous
798 /// (possibly larger) table cap, so emitting is a correctness
799 /// requirement, not a nicety — see the RFC 9113 encoder-decoder
800 /// synchronisation contract (§6.5.2).
801 pub pending_table_size_update: Option<u32>,
802 /// `true` when the stream parked in `expect_write` still holds, in its
803 /// `kawa.out`, a HEADERS or CONTINUATION frame the wire has not started.
804 ///
805 /// That block was HPACK-encoded when it was prepared, so the encoder's
806 /// dynamic table already holds its insertions. Only the parked stream can
807 /// hold unsent frames — a pass flushes each stream it prepares before the
808 /// next one and stops at the first stall — so if the next write pass
809 /// finds the park gone (the stream was removed) or its `kawa.out` empty
810 /// (a default answer or a forced termination cleared it), that block was
811 /// dropped and never will reach the peer, and the pass resets the
812 /// encoder's table before any other block is encoded
813 /// ([`Self::reset_encoder_table`], sozu-proxy/sozu#1627). Set where a
814 /// write pass parks a stream, cleared by the next pass's
815 /// `H2WritePhase::Start`.
816 parked_header_block: bool,
817 /// The DATA payload octets the stream parked in `expect_write` still
818 /// holds, whole and unsent, in its `kawa.out`
819 /// ([`h2_transmit::queued_data_payload`]).
820 ///
821 /// `H2WritePhase::Prepare` debits both send windows when it encodes DATA,
822 /// before any of it is written. The same "only the parked stream holds
823 /// unsent frames" argument as [`Self::parked_header_block`] makes this
824 /// the whole of the credit spent on DATA the wire has not taken. When
825 /// those frames are dropped instead of sent — the stream removed, or its
826 /// `kawa.out` cleared — the peer never receives them and never returns
827 /// their credit, so the connection gives it back itself
828 /// ([`Self::refund_parked_data`], sozu-proxy/sozu#1641): eagerly in
829 /// [`Self::remove_dead_stream`], otherwise at the next pass's
830 /// `H2WritePhase::Start`. Set with `parked_header_block`, taken by
831 /// whichever comes first.
832 parked_data: i32,
833 /// Whether the one-shot stream-0 WINDOW_UPDATE that enlarges the
834 /// connection receive window to `H2ConnectionConfig::initial_connection_window`
835 /// was sent. A backend may send SETTINGS more than once; granting the
836 /// surplus again each time would push its send window past 2^31-1
837 /// (RFC 9113 §6.9.1).
838 connection_window_enlarged: bool,
839 /// RFC 9113 §6.8 double-GOAWAY drain bookkeeping, encapsulated so
840 /// nothing outside `h2_drain.rs` can reach the raw fields — see
841 /// [`h2_drain::H2DrainState`].
842 pub(super) drain: h2_drain::H2DrainState,
843 /// The read landing zone for every frame header and every stream-0
844 /// frame's own payload bytes, for the duration of ONE frame's read. It is
845 /// input only: control frames are serialised into `Self::output`
846 /// (#1604), and a HEADERS+CONTINUATION block is reassembled in
847 /// `Self::header_reassembly`; see that field's doc and
848 /// `h2_header_reassembly.rs` for why, and LIFECYCLE.md invariant 24 for
849 /// the bugs the two splits close.
850 pub zero: GenericHttpStream,
851 /// The single ordered output queue: every control frame, and the unsent
852 /// rest of the one stream frame a partial write cut, each queued whole.
853 /// Stream transmits send it first. See `h2_output.rs`.
854 output: h2_output::H2Output,
855 /// READABLE interest was withdrawn because [`Self::output`] reached the
856 /// read cap (`Self::output_read_cap`), and is restored once it drains
857 /// below it — HAProxy's `H2_CF_DEM_MROOM`.
858 reads_wait_for_output: bool,
859 /// Owned accumulator for an in-progress HEADERS+CONTINUATION field
860 /// block (`H2State::ContinuationHeader`/`ContinuationFrame`). Separated
861 /// from [`Self::zero`] so a control-frame flush that clears and reuses
862 /// `zero.storage` has no way to name — and therefore cannot corrupt —
863 /// the bytes reassembled so far. See `h2_header_reassembly.rs` and
864 /// LIFECYCLE.md invariant 24.
865 header_reassembly: h2_header_reassembly::HeaderBlockAccumulator,
866 /// Byte accounting for connection overhead attribution.
867 pub bytes: H2ByteAccounting,
868 /// CVE-mitigation flood/abuse counters (Rapid Reset, MadeYouReset,
869 /// CONTINUATION, Ping, Settings floods), encapsulated so nothing outside
870 /// `h2_flood_detector.rs` can reach the raw fields — see
871 /// [`h2_flood_detector::H2FloodDetector`].
872 flood_detector: h2_flood_detector::H2FloodDetector,
873 /// RFC 9113 §6.5: timestamp when we sent SETTINGS and are awaiting ACK.
874 /// If the peer does not ACK within SETTINGS_ACK_TIMEOUT, we send GOAWAY
875 /// with SettingsTimeout error.
876 pub settings_sent_at: Option<Instant>,
877 /// Queued proxy-emitted RST_STREAM frames, their pending-queue bound and
878 /// the lifetime count the session log reports,
879 /// encapsulated so nothing outside `h2_control_tx.rs` can reach the raw
880 /// fields — see [`h2_control_tx::H2ControlTx`]. Frames are queued while
881 /// refusing streams during `readable()`; the write happens in the
882 /// writable preamble so it cannot conflict with `zero.storage`'s use for
883 /// frame-payload discard.
884 control_tx: h2_control_tx::H2ControlTx,
885 /// Set by [`Self::refuse_stream_and_discard`], consumed once by the
886 /// `H2State::Discard` arm of [`Self::handle_read`]. Carries enough of the
887 /// refused frame's shape to still hand the connection-level HPACK
888 /// decoder a complete field block before its bytes are dropped — RFC
889 /// 9113 §4.3: field-compression state is scoped to the connection, not
890 /// the stream. See `LIFECYCLE.md`'s Discard section.
891 discarded_field_block: Option<(StreamId, DiscardedFieldBlock)>,
892 /// The stream and field-block bytes of a discarded header block whose
893 /// frame did not carry END_HEADERS: set by the `H2State::Discard` arm,
894 /// consumed by `handle_header_state`, where only a CONTINUATION frame
895 /// on that stream may come next (RFC 9113 §6.10). The block is decoded
896 /// once whole, to keep the HPACK dynamic table in sync (§4.3).
897 pending_discarded_block: Option<(StreamId, Vec<u8>)>,
898 /// True once we've asked rustls to emit TLS close_notify for this frontend.
899 close_notify_sent: bool,
900 /// The last socket write of the current [`H2Shell::writable`] pass
901 /// answered `WouldBlock`: the kernel refused it, so it owes this socket
902 /// its next WRITABLE edge, and [`Self::ensure_tls_flushed`] must not
903 /// synthesize one. Recorded by [`Self::note_write_status`] after every
904 /// write, and cleared when a pass starts, so a pass that wrote nothing
905 /// re-arms as before (sozu-proxy/sozu#1788).
906 kernel_refused_write: bool,
907 /// Per-listener H2 connection tuning (window size, max streams, shrink ratio).
908 pub connection_config: H2ConnectionConfig,
909 /// Maximum pending WINDOW_UPDATE entries before dropping.
910 /// Derived from `connection_config.max_concurrent_streams` at construction.
911 max_pending_window_updates: usize,
912 /// Ready incremental streams observed on the last completed write pass,
913 /// summed across urgency buckets (RFC 9218 §4). Sampled at the END of
914 /// `write_streams` from [`h2_scheduler::ReadyIncrementalCensus::ready_total`],
915 /// where the pass census is final: the entry call to
916 /// [`Self::gauge_connection_state`] runs *before* that census exists, so
917 /// sampling there would publish the previous pass's value.
918 ///
919 /// Carried as connection state rather than emitted inline because the
920 /// aggregate it feeds must be a signed delta, not an absolute set — see
921 /// [`Self::gauge_connection_state`].
922 ready_incremental_streams: usize,
923 /// Last `(connection_window, active_streams, pending_window_updates,
924 /// ready_incremental_streams)` snapshot emitted by
925 /// [`Self::gauge_connection_state`]. The snapshot represents this
926 /// connection's *contribution* to the four aggregate gauges; each call
927 /// pushes the signed delta against this snapshot onto
928 /// [`Self::metric_events`] so the gauges sum across connections.
929 ///
930 /// Stays `None` until the first emission.
931 /// [`Self::release_connection_gauges`] applies the negative of this
932 /// snapshot so the connection's contribution is always rebalanced to zero
933 /// on teardown — independent of which close path runs. `H2Shell`'s
934 /// [`Drop`] is what calls it.
935 last_gauge_snapshot: Option<(usize, usize, usize, usize)>,
936 /// Metric events this core produced and has not yet handed to its shell.
937 ///
938 /// The core owns the *arithmetic* — which metric, and by how much — and
939 /// nothing else: writing to the worker-local `METRICS` aggregator is a
940 /// reach for ambient process state that a byte-in / byte-out core cannot
941 /// make, and that a deterministic simulator cannot observe. So the events
942 /// queue here and [`H2Shell`] drains them through
943 /// [`Self::drain_metric_events`], exactly as `protocol/udp/`'s core
944 /// queues `Output::Metric` for `crate::udp` to drain.
945 ///
946 /// Nothing is lost if a pass forgets to drain: the queue is FIFO and the
947 /// values are signed deltas, so a later drain records the same sum, and
948 /// `H2Shell`'s [`Drop`] drains whatever is left. See
949 /// [`H2Shell::record_pending_metrics`].
950 metric_events: Vec<MetricEvent>,
951 /// Per-stream idle cap. Streams with no activity for longer than this are
952 /// RST_STREAM(CANCEL)'d by [`Self::cancel_timed_out_streams`]. Compared
953 /// against `stream_table`'s per-stream activity/flow-control-stall
954 /// caches — see `h2_stream_table::H2StreamTable::collect_timed_out`.
955 pub stream_idle_timeout: std::time::Duration,
956 /// RFC 9113 §5.1.2 back-pressure: count of stream refusals
957 /// (REFUSED_STREAM emitted via [`Self::refuse_stream_and_discard`]) within
958 /// the current back-pressure window. When the count exceeds
959 /// [`BACKPRESSURE_REFUSAL_THRESHOLD`] inside one
960 /// [`BACKPRESSURE_WINDOW_DURATION`] we halve the advertised
961 /// `SETTINGS_MAX_CONCURRENT_STREAMS` to signal the peer to slow down.
962 refuse_count_window: u32,
963 /// Start timestamp for the current back-pressure window.
964 refuse_window_start: Instant,
965 /// Set once we have halved `local_settings.settings_max_concurrent_streams`
966 /// in response to a refusal burst. Prevents the cap from collapsing to 0
967 /// on sustained abuse — a single halving per connection is sufficient to
968 /// signal back-pressure; further bursts trigger `EnhanceYourCalm`.
969 mcs_backpressure_applied: bool,
970 /// First and last client stream ids of the latest run of streams refused
971 /// under flood pressure (`H2FloodDetector::refuses_new_streams`,
972 /// `lib/src/protocol/mux/h2_flood_detector.rs`). Every new client stream
973 /// is refused while the soft state holds, so one run is a contiguous
974 /// range; the next accepted stream ends it, and the next refusal starts
975 /// a new run. Only that latest run is tracked: ids refused in an earlier
976 /// run are forgotten.
977 ///
978 /// A RST_STREAM the client sends for one of these ids resets a stream
979 /// that never got a response, so `Self::handle_rst_stream_frame` counts
980 /// it as a pre-response reset. Without that, a client that ignores the
981 /// refusals and keeps opening and resetting streams would stop moving
982 /// toward the pre-response cap as soon as the soft state engages. A
983 /// reset of an id from an earlier run takes the untracked-stream arm
984 /// there instead and counts as a reset after a response, as it would
985 /// without the soft state.
986 flood_refused_streams: Option<(StreamId, StreamId)>,
987 /// Set once the client resets a stream of [`Self::flood_refused_streams`]:
988 /// it does not honour `REFUSED_STREAM`, so no further stream is refused
989 /// under flood pressure on this connection and it meets the flood limits
990 /// unchanged. Refusing it further would only add one RST_STREAM per
991 /// stream it opens ahead of the pre-response cap's GOAWAY.
992 ///
993 /// A benign race sets it too: a client that cancels a stream just as
994 /// Sōzu refuses it sends a RST_STREAM that crosses the `REFUSED_STREAM`.
995 /// The flag is never cleared, so such a connection behaves for the rest
996 /// of its life as it would without the soft state.
997 flood_refusal_ignored: bool,
998 /// Clock snapshot for the pass currently executing — this connection's
999 /// mirror of [`Context::now`](super::Context::now).
1000 ///
1001 /// Every time-based decision in this module reads this field. The only
1002 /// code below [`Mux`](super::Mux) that samples the real clock is
1003 /// [`Self::new`], which takes one sample to seed this field,
1004 /// `refuse_window_start` and the flood detector's window — a connection
1005 /// is constructed outside any pass, so there is no snapshot to inherit.
1006 /// `h2_flood_detector::H2FloodDetector` carries no clock-sampling
1007 /// exception of its own any more: its former test-only `impl Default`
1008 /// was removed in the same changeset that extracted it, so
1009 /// [`Self::new`] is the sole place under `ConnectionH2` that reads
1010 /// the system clock.
1011 ///
1012 /// Assigned from `context.now` at each public entry point
1013 /// ([`H2Shell::readable`], [`H2Shell::writable`],
1014 /// [`Self::cancel_timed_out_streams`], [`Self::start_stream`]) and from
1015 /// the `now` parameter of [`Self::graceful_goaway`]. `Mux::shutting_down`
1016 /// reaches it indirectly: it refreshes `Context::now`, and the
1017 /// `readable()` call inside `drive_frontend_shutdown_io` mirrors that into
1018 /// this field before the forced-close deadline is evaluated.
1019 ///
1020 /// A field rather than a threaded parameter because the read sites are
1021 /// unreachable from a `context`: [`Self::handle_ping_frame`] takes no
1022 /// context at all, and the ten `check_flood_or_return!` sites are spread
1023 /// across six frame handlers. The macro reads `$self.now`, so those ten
1024 /// call sites stay as they were.
1025 pub(super) now: Instant,
1026 /// A [`Self::force_disconnect`] raised at a site with no socket in reach,
1027 /// waiting for the shell to read `tls_wants_write` and settle it through
1028 /// [`Self::force_disconnect_after_query`].
1029 ///
1030 /// `None` is the resting state and the only state a connection is in
1031 /// between passes: every entry point that can raise one settles before
1032 /// returning, so this never survives a pass. See
1033 /// [`H2ForceDisconnectTarget`] for why the target is parked here rather
1034 /// than returned.
1035 pending_force_disconnect: Option<H2ForceDisconnectTarget>,
1036 /// How long the lingering close after a final GOAWAY may drain: the
1037 /// listener's `request_timeout`, which a frontend connection is built
1038 /// with. Unused on a backend connection.
1039 linger_timeout: Duration,
1040 /// Lingering-close state, set by [`H2Shell::writable`] when the
1041 /// connection closes after its final GOAWAY. See
1042 /// [`super::shared::Linger`].
1043 pub(super) linger: Linger,
1044 /// Whether the final GOAWAY this connection queued carried `NO_ERROR`.
1045 /// Selects the lingering close's bounds ([`Self::linger_budget`]): one
1046 /// that answers a protocol error or a flood lingers only briefly.
1047 pub(super) graceful_goaway: bool,
1048}
1049/// Renders the peer address this connection snapshotted at construction,
1050/// where it used to render the socket behind it.
1051///
1052/// `socket_ref()` handed `mio::net::TcpStream`'s own `Debug` a concrete OS
1053/// handle — local address, peer address and file descriptor — which is more
1054/// than a reader of this struct needs and is a value a byte-in / byte-out core
1055/// cannot produce. `peer_address` is what every `log_context!` line already
1056/// renders, it survives the peer's reset where a live `getpeername(2)` answers
1057/// `ENOTCONN`, and it carries no descriptor. In the H2 simulator it is a fixed
1058/// address by construction, so no ephemeral port reaches a trace through here
1059/// any more.
1060///
1061/// No production body in this file reaches `SocketHandler::socket_ref` any
1062/// more, and `socket_mut` never had one. `ConnectionH2::snapshot_rtts` held
1063/// the last of them until issue #1339 Q11 moved the frontend RTT read out to
1064/// the shell (`ShellEndpoint::local_rtt` since #1590); the remaining spellings
1065/// are all in `mod tests`.
1066impl std::fmt::Debug for ConnectionH2 {
1067 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1068 f.debug_struct("ConnectionH2")
1069 .field("position", &self.position)
1070 .field("state", &self.state)
1071 .field("expect", &self.stream_table.expect_read())
1072 .field("readiness", &self.readiness)
1073 .field("local_settings", &self.local_settings)
1074 .field("peer_settings", &self.peer_settings)
1075 .field("peer_address", &self.peer_address)
1076 .field("streams", self.stream_table.streams())
1077 .field("zero", &self.zero.storage.meter(20))
1078 .field(
1079 "header_reassembly_in_progress",
1080 &self.header_reassembly.is_in_progress(),
1081 )
1082 .field("header_reassembly_len", &self.header_reassembly.len())
1083 .field("window", &self.flow_control.window())
1084 .field(
1085 "total_rst_streams_queued",
1086 &self.control_tx.lifetime_queued(),
1087 )
1088 .finish()
1089 }
1090}
1091
1092/// One metric the H2 core asks its shell to record.
1093///
1094/// The metric macros of [`crate::metrics`] borrow the worker-local `METRICS`
1095/// aggregator through a `thread_local!`, which is exactly the ambient process
1096/// reach a byte-in / byte-out core gave up — and which a deterministic
1097/// simulator has no way to observe, so a core that wrote there could not be
1098/// driven by one. [`ConnectionH2`] therefore returns the metric instead of
1099/// emitting it, and [`H2Shell`] — the layer that already owns the socket, and
1100/// the layer `METRICS` legitimately belongs to — records it through
1101/// [`record_metric`].
1102///
1103/// The precedent is in-repo and predates this type: `protocol/udp/`'s core has
1104/// zero metric-macro sites and emits `Output::Metric(MetricEvent)`, which
1105/// `crate::udp`'s `Udp::record_metric` folds into the aggregator and
1106/// `sim/tests/udp_simulation.rs` folds into a shadow model instead.
1107///
1108/// Variants name what happened in the core's own terms; mapping them onto
1109/// metric keys is [`record_metric`]'s job and happens shell-side. The four
1110/// below are the H2 connection-level aggregate gauges, each carrying the
1111/// *signed delta* this connection contributes — never an absolute value. The
1112/// delta is what makes them summable across every live H2 connection; an
1113/// absolute `gauge!` would have each connection overwrite the last one's
1114/// value, which is the defect [`ConnectionH2::gauge_connection_state`]'s
1115/// snapshot arithmetic was introduced to fix.
1116///
1117/// The migration is incremental: a site that still calls a metric macro from
1118/// the core has not moved yet, and the two mechanisms compose — both end in
1119/// the same aggregator, so a half-migrated counter is not a wrong counter.
1120/// The type lives here rather than in a `protocol/mux/` module of its own
1121/// because every variant is H2-connection state; the first non-H2 core type
1122/// that needs one is what should promote it.
1123///
1124/// **`protocol::udp::MetricEvent` is a different type with the same name, and
1125/// deliberately so.** It is the UDP core's own vocabulary — flows admitted,
1126/// datagrams forwarded — and shares no variant with this one, nor any prospect
1127/// of sharing one. Converging them would force `crate::udp` and
1128/// `sim/tests/udp_simulation.rs` to match on H2 connection gauges, because
1129/// `protocol::udp::Output::Metric` embeds the type. The shared name is this
1130/// tree's existing idiom for "same role at the same boundary, different
1131/// protocol": `protocol::tcp_preread::Output` and `protocol::udp::Output`
1132/// already do exactly this, and `RejectReason`, `BackendStatus`, `Router` and
1133/// `SessionStatus` are each declared twice under `lib/src/` for the same
1134/// reason.
1135#[derive(Debug, Clone, Copy, PartialEq)]
1136pub(super) enum MetricEvent {
1137 /// Signed delta on [`names::h2::CONNECTION_WINDOW_BYTES`] — this
1138 /// connection's available connection-level send window, clamped at 0 so
1139 /// the aggregate represents capacity rather than deficit.
1140 ConnectionWindowBytes(i64),
1141 /// Signed delta on [`names::h2::CONNECTION_ACTIVE_STREAMS`] — this
1142 /// connection's in-flight stream count.
1143 ConnectionActiveStreams(i64),
1144 /// Signed delta on [`names::h2::CONNECTION_PENDING_WINDOW_UPDATES`] —
1145 /// this connection's queued, un-flushed per-stream WINDOW_UPDATE entries.
1146 ConnectionPendingWindowUpdates(i64),
1147 /// Signed delta on [`names::h2::STREAMS_READY_INCREMENTAL_BY_URGENCY`] —
1148 /// this connection's streams that were ready to emit AND marked
1149 /// incremental (RFC 9218 §4) on its last completed write pass.
1150 StreamsReadyIncrementalByUrgency(i64),
1151
1152 // ── Frames written to the peer ──────────────────────────────────────
1153 /// A SETTINGS frame was queued in the connection's output.
1154 SettingsFrameSent,
1155 /// A PING ACK was queued in the connection's output.
1156 PingAckFrameSent,
1157 /// A GOAWAY frame was serialised. Deliberately **not** folded into
1158 /// [`Self::GoAwaySent`]: the decision to go away is unconditional, while
1159 /// this fires only once `serializer::gen_goaway` succeeded, and a
1160 /// serialisation failure must not be recorded as a frame on the wire.
1161 GoAwayFrameSent,
1162 /// The connection decided to send GOAWAY with this error code, whether or
1163 /// not the frame then serialised.
1164 GoAwaySent(H2Error),
1165 /// A RST_STREAM was serialised with this error code. One event for two
1166 /// counters — the frame-type total and the per-code breakdown — because
1167 /// both macro lines it replaces were unconditional and adjacent.
1168 RstStreamSent(H2Error),
1169 /// `n` queued WINDOW_UPDATE frames were flushed in one control pass.
1170 WindowUpdateFramesSent(i64),
1171
1172 // ── Frames read from the peer ───────────────────────────────────────
1173 /// A frame arrived and parsed. The shell maps the type onto the
1174 /// `h2.frames.rx.*` breakdown.
1175 FrameReceived(FrameType),
1176 /// A RST_STREAM arrived carrying this **raw wire** error code. Raw rather
1177 /// than [`H2Error`] because a code outside RFC 9113 §7 has its own
1178 /// `…unknown_error` bucket, and that mapping is the shell's.
1179 RstStreamReceived(u32),
1180 /// The peer reset a stream before the backend had begun answering it.
1181 RstStreamReceivedBeforeResponse,
1182 /// A new client stream was refused with `REFUSED_STREAM` because the
1183 /// connection is in the soft state below the pre-response RST_STREAM cap.
1184 StreamRefusedUnderFlood,
1185 /// A GOAWAY arrived carrying this raw wire error code.
1186 GoAwayReceived(u32),
1187
1188 // ── Control-queue back-pressure ─────────────────────────────────────
1189 /// A WINDOW_UPDATE was dropped because the control queue was full.
1190 WindowUpdateDropped,
1191 /// A proxy-emitted RST_STREAM was dropped for the same reason.
1192 RstStreamDropped,
1193
1194 // ── Stream lifecycle ────────────────────────────────────────────────
1195 /// A request completed end to end over H2.
1196 EndToEndH2Request,
1197 /// A stream was reaped because its flow-control window had stalled.
1198 StreamReapedWindowStall,
1199 /// A reaped stalled stream had also consumed cumulative stall budget. A
1200 /// strict subset of [`Self::StreamReapedWindowStall`], emitted alongside
1201 /// it rather than instead of it.
1202 StreamReapedStallBudget,
1203 /// A stream was reaped for exceeding the per-stream idle cap.
1204 StreamReapedIdleTimeout,
1205 /// HEADERS arrived that no stream could be allocated for.
1206 HeadersWithoutStream,
1207 /// A header block failed to parse on a backend (client-position)
1208 /// connection.
1209 BackendHeaderParseError,
1210 /// A header block failed to parse on a frontend (server-position)
1211 /// connection.
1212 FrontendHeaderParseError,
1213
1214 // ── Readiness re-arm sites ──────────────────────────────────────────
1215 /// Writable readiness was re-armed because the control queue still owes
1216 /// the peer bytes.
1217 WritableRearmedControlQueue,
1218 /// …because peer DATA has to be forwarded.
1219 WritableRearmedPeerData,
1220 /// …because peer HEADERS have to be forwarded.
1221 WritableRearmedPeerHeaders,
1222 /// …because a PRIORITY_UPDATE changed the scheduler's order.
1223 WritableRearmedPriorityUpdate,
1224
1225 // ── Flood mitigation ────────────────────────────────────────────────
1226 /// A flood threshold tripped. Carries the key rather than a semantic
1227 /// discriminant because `H2FloodViolation` already holds `metric_key`
1228 /// beside `reason` in one field, chosen at the construction site
1229 /// precisely so the log line and the counter cannot drift apart. A
1230 /// semantic variant here would duplicate that mapping and reintroduce the
1231 /// drift the single field exists to prevent — the key is domain data the
1232 /// core already receives, not something synthesised at the metric site.
1233 FloodViolation { metric_key: &'static str },
1234
1235 // ── Wire conversion (`converter::H2BlockConverter`) ─────────────────
1236 /// A DATA frame was written for a stream.
1237 DataFrameSent,
1238 /// A HEADERS frame was written for a stream.
1239 HeadersFrameSent,
1240 /// A CONTINUATION frame was written (RFC 9113 §6.10 header-block split).
1241 ContinuationFrameSent,
1242 /// A stream could not emit because its flow-control window was exhausted.
1243 FlowControlStall,
1244 /// The same stall, on a connection writing toward a backend. Scoped by
1245 /// `H2BlockConverter::position_is_client`, captured at converter
1246 /// construction so the stall sites do not each need the position.
1247 BackendFlowControlPaused,
1248 /// A header block was refused for exceeding the peer's declared budget.
1249 HeadersRejectedBudgetOverrun,
1250
1251 // ── Header decoding (`pkawa`) ───────────────────────────────────────
1252 /// A header was rejected by H2 header policy. Carries the per-reason key
1253 /// alongside the roll-up, for the same reason [`Self::FloodViolation`]
1254 /// does: `reject_metric_key!` derives it from the `RejectReason` variant
1255 /// at the rejection site, and re-deriving it shell-side would duplicate a
1256 /// mapping that exists once.
1257 HeaderRejected { reason_key: &'static str },
1258 /// A trailer field that could spoof a client-identity header was elided.
1259 TrailerSpoofVectorElided,
1260 /// A request trailer field named in `TRAILER_FORBIDDEN_FIELDS` was elided.
1261 TrailerForbiddenFieldElided,
1262
1263 // ── Shared leaves reached from the core ─────────────────────────────
1264 /// Bytes read on a frontend connection.
1265 FrontendBytesIn(i64),
1266 /// Bytes read on a backend connection.
1267 BackendBytesIn(i64),
1268 /// Bytes written on a frontend connection.
1269 FrontendBytesOut(i64),
1270 /// Bytes written on a backend connection.
1271 BackendBytesOut(i64),
1272 /// A request started being served. Records the lifetime counter and the
1273 /// in-flight gauge's `+1` together: `ConnectionH2::handle_headers_frame`
1274 /// emits them on consecutive unconditional lines, which is the same
1275 /// criterion that allowed [`Self::RstStreamSent`] to fold its pair and
1276 /// that forbade folding the GOAWAY one.
1277 RequestStarted,
1278 /// A request finished being served — the in-flight gauge's `-1`. Its
1279 /// `+1` counterpart is [`Self::RequestStarted`] on the H2 path and a
1280 /// `gauge_add!` macro on the H1 path, because `ConnectionH1` is its own
1281 /// shell and `METRICS` is legitimately its own. The pair is therefore
1282 /// deliberately split across two mechanisms; what makes that safe is that
1283 /// each PATH balances, not that the pair travels together.
1284 ActiveRequestFinished,
1285 /// An access log could not be written.
1286 AccessLogUnsent,
1287}
1288
1289/// Fold one core-returned [`MetricEvent`] into the worker-local `METRICS`
1290/// aggregator.
1291///
1292/// Shell-side by construction, and deliberately the only place in this module
1293/// that turns a [`MetricEvent`] into a metric macro. [`ConnectionH2`] never
1294/// calls it: it queues events and lets the layer that owns the socket decide
1295/// when they are recorded. A second translation site would be a second place
1296/// for the gauge arithmetic to drift.
1297pub(super) fn record_metric(event: MetricEvent) {
1298 match event {
1299 MetricEvent::ConnectionWindowBytes(delta) => {
1300 gauge_add!(names::h2::CONNECTION_WINDOW_BYTES, delta)
1301 }
1302 MetricEvent::ConnectionActiveStreams(delta) => {
1303 gauge_add!(names::h2::CONNECTION_ACTIVE_STREAMS, delta)
1304 }
1305 MetricEvent::ConnectionPendingWindowUpdates(delta) => {
1306 gauge_add!(names::h2::CONNECTION_PENDING_WINDOW_UPDATES, delta)
1307 }
1308 MetricEvent::StreamsReadyIncrementalByUrgency(delta) => {
1309 gauge_add!(names::h2::STREAMS_READY_INCREMENTAL_BY_URGENCY, delta)
1310 }
1311
1312 MetricEvent::SettingsFrameSent => incr!(names::h2::FRAMES_TX_SETTINGS),
1313 MetricEvent::PingAckFrameSent => incr!(names::h2::FRAMES_TX_PING_ACK),
1314 MetricEvent::GoAwayFrameSent => incr!(names::h2::FRAMES_TX_GOAWAY),
1315 MetricEvent::GoAwaySent(error) => count!(metric_for_goaway_sent(error), 1),
1316 MetricEvent::RstStreamSent(error) => {
1317 incr!(names::h2::FRAMES_TX_RST_STREAM);
1318 count!(metric_for_rst_stream_sent(error), 1);
1319 }
1320 MetricEvent::WindowUpdateFramesSent(frames) => {
1321 count!(names::h2::FRAMES_TX_WINDOW_UPDATE, frames)
1322 }
1323
1324 MetricEvent::FrameReceived(frame_type) => {
1325 count!(h2_frame_rx_metric_key(frame_type), 1)
1326 }
1327 MetricEvent::RstStreamReceived(error_code) => {
1328 count!(metric_for_rst_stream_received(error_code), 1)
1329 }
1330 MetricEvent::RstStreamReceivedBeforeResponse => {
1331 count!(names::h2::RST_STREAM_RECEIVED_PRE_RESPONSE_START, 1)
1332 }
1333 MetricEvent::StreamRefusedUnderFlood => {
1334 count!(names::h2::FLOOD_STREAM_REFUSED, 1)
1335 }
1336 MetricEvent::GoAwayReceived(error_code) => {
1337 count!(metric_for_goaway_received(error_code), 1)
1338 }
1339
1340 MetricEvent::WindowUpdateDropped => incr!(names::h2::WINDOW_UPDATE_DROPPED),
1341 MetricEvent::RstStreamDropped => incr!(names::h2::RST_STREAM_DROPPED),
1342
1343 MetricEvent::EndToEndH2Request => incr!(names::http::E2E_H2),
1344 MetricEvent::StreamReapedWindowStall => {
1345 count!(names::h2::STREAMS_REAPED_WINDOW_STALL, 1)
1346 }
1347 MetricEvent::StreamReapedStallBudget => {
1348 count!(names::h2::STREAMS_REAPED_STALL_BUDGET, 1)
1349 }
1350 MetricEvent::StreamReapedIdleTimeout => {
1351 count!(names::h2::STREAMS_REAPED_IDLE_TIMEOUT, 1)
1352 }
1353 MetricEvent::HeadersWithoutStream => incr!(names::h2::HEADERS_NO_STREAM_ERROR),
1354 MetricEvent::BackendHeaderParseError => incr!(names::http::BACKEND_PARSE_ERRORS),
1355 MetricEvent::FrontendHeaderParseError => incr!(names::http::FRONTEND_PARSE_ERRORS),
1356
1357 MetricEvent::WritableRearmedControlQueue => {
1358 incr!(names::h2::SIGNAL_WRITABLE_REARMED_CONTROL_QUEUE)
1359 }
1360 MetricEvent::WritableRearmedPeerData => {
1361 incr!(names::h2::SIGNAL_WRITABLE_REARMED_PEER_DATA)
1362 }
1363 MetricEvent::WritableRearmedPeerHeaders => {
1364 incr!(names::h2::SIGNAL_WRITABLE_REARMED_PEER_HEADERS)
1365 }
1366 MetricEvent::WritableRearmedPriorityUpdate => {
1367 incr!(names::h2::SIGNAL_WRITABLE_REARMED_PRIORITY_UPDATE)
1368 }
1369
1370 MetricEvent::FloodViolation { metric_key } => count!(metric_key, 1),
1371
1372 MetricEvent::DataFrameSent => incr!(names::h2::FRAMES_TX_DATA),
1373 MetricEvent::HeadersFrameSent => incr!(names::h2::FRAMES_TX_HEADERS),
1374 MetricEvent::ContinuationFrameSent => incr!(names::h2::FRAMES_TX_CONTINUATION),
1375 MetricEvent::FlowControlStall => incr!(names::h2::FLOW_CONTROL_STALL),
1376 MetricEvent::BackendFlowControlPaused => {
1377 incr!(names::backend::FLOW_CONTROL_PAUSED)
1378 }
1379 MetricEvent::HeadersRejectedBudgetOverrun => {
1380 incr!(names::h2::HEADERS_REJECTED_BUDGET_OVERRUN)
1381 }
1382
1383 MetricEvent::HeaderRejected { reason_key } => {
1384 incr!(names::h2::HEADERS_REJECTED_TOTAL);
1385 incr!(reason_key);
1386 }
1387 MetricEvent::TrailerSpoofVectorElided => {
1388 incr!(names::h2::TRAILER_SPOOF_VECTOR_ELIDED)
1389 }
1390 MetricEvent::TrailerForbiddenFieldElided => {
1391 incr!(names::h2::TRAILER_FORBIDDEN_FIELD_ELIDED)
1392 }
1393
1394 MetricEvent::FrontendBytesIn(bytes) => count!(names::backend::BYTES_IN, bytes),
1395 MetricEvent::BackendBytesIn(bytes) => count!(names::backend::BACK_BYTES_IN, bytes),
1396 MetricEvent::FrontendBytesOut(bytes) => count!(names::backend::BYTES_OUT, bytes),
1397 MetricEvent::BackendBytesOut(bytes) => {
1398 count!(names::backend::BACK_BYTES_OUT, bytes)
1399 }
1400 MetricEvent::RequestStarted => {
1401 incr!(names::http::REQUESTS);
1402 gauge_add!(names::http::ACTIVE_REQUESTS, 1);
1403 }
1404 MetricEvent::ActiveRequestFinished => {
1405 gauge_add!(names::http::ACTIVE_REQUESTS, -1)
1406 }
1407 MetricEvent::AccessLogUnsent => incr!(names::access_logs::UNSENT),
1408 }
1409}
1410
1411#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1412pub enum H2StreamId {
1413 Zero,
1414 Other { id: StreamId, gid: GlobalStreamId },
1415}
1416
1417/// What [`ConnectionH2::poll_read_target`] wants its caller to do before it
1418/// may call [`ConnectionH2::handle_read`].
1419///
1420/// The three variants are the three shapes one frontend read pass takes, and
1421/// [`Self::Skip`] is deliberately not folded into a `Fill { amount: 0 }`: that
1422/// would have the caller issue a zero-length read, and
1423/// `update_readiness_after_read` reads its `(0, SocketResult::Continue)`
1424/// answer as "nothing arrived, stop" — so every frame carrying no payload (an
1425/// empty SETTINGS, an empty DATA, a SETTINGS ACK) would stop being parsed at
1426/// all. The variant is what stops a caller from having to know that.
1427#[derive(Debug, Clone, Copy)]
1428pub enum H2ReadTarget {
1429 /// The core ended the pass by itself: a SETTINGS-ACK timeout, nothing owed
1430 /// by the peer, or no room left for what is owed. No read, and no
1431 /// [`ConnectionH2::handle_read`] — this is the pass's result.
1432 Done(MuxResult),
1433 /// Every byte the core is waiting for already sits in `stream_id`'s
1434 /// buffer, because the frame in flight carries a zero-length payload.
1435 /// Perform no read and answer with [`H2ReadOutcome::Skipped`].
1436 Skip(H2StreamId),
1437 /// Read into the space `read_space` returns for `stream_id`, which is
1438 /// exactly `amount` bytes and never less than one, then answer with
1439 /// [`H2ReadOutcome::Filled`].
1440 Fill {
1441 stream_id: H2StreamId,
1442 amount: usize,
1443 },
1444}
1445
1446/// What the caller of [`ConnectionH2::poll_read_target`] actually did, handed
1447/// back to [`ConnectionH2::handle_read`]. Each variant answers the
1448/// [`H2ReadTarget`] of the same name.
1449#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1450pub enum H2ReadOutcome {
1451 /// Answers [`H2ReadTarget::Skip`]: no read was performed.
1452 Skipped,
1453 /// Answers [`H2ReadTarget::Fill`]: `amount` bytes of space were offered,
1454 /// and the socket returned `size` bytes with `status`.
1455 ///
1456 /// `amount` is echoed back rather than re-read from `expect_read` inside
1457 /// [`ConnectionH2::handle_read`], so the byte debt the core settles is the
1458 /// one it actually offered and not whatever the table happens to hold by
1459 /// then.
1460 Filled {
1461 amount: usize,
1462 size: usize,
1463 status: SocketResult,
1464 },
1465}
1466
1467/// The buffer an [`H2ReadTarget`] names, resolved to the `Kawa` owning it.
1468///
1469/// [`H2StreamId::Zero`] is the connection-level scratch every control frame
1470/// and every header block is read into; [`H2StreamId::Other`] is one
1471/// application stream's *read* buffer, which [`Stream::split`] reports as the
1472/// front buffer at [`Position::Server`] and the back buffer at
1473/// [`Position::Client`].
1474///
1475/// Taking `zero` and `streams` apart instead of a whole `&mut ConnectionH2`
1476/// and `&mut Context` is what makes the split compile: `ConnectionH2::socket`
1477/// and `Context::debug` stay borrowable while the buffer this returns is live,
1478/// which a method returning a borrow of all of `*self` would forbid.
1479fn read_buffer<'a>(
1480 zero: &'a mut GenericHttpStream,
1481 streams: &'a mut [Stream],
1482 position: &Position,
1483 stream_id: H2StreamId,
1484) -> &'a mut GenericHttpStream {
1485 match stream_id {
1486 H2StreamId::Zero => zero,
1487 H2StreamId::Other {
1488 gid: global_stream_id,
1489 ..
1490 } => streams[global_stream_id].split(position).rbuffer,
1491 }
1492}
1493
1494/// What [`ConnectionH2::poll_write_target`] wants its caller to do before it
1495/// may call [`ConnectionH2::handle_write`].
1496///
1497/// The write half of [`H2ReadTarget`]'s protocol, with ONE forced divergence:
1498/// a `readable()` pass performs exactly one `socket_read`, so the read side is
1499/// a two-call protocol that ENDS with `handle_read`. A write pass performs an
1500/// unbounded number of `socket_write_vectored` calls — the pre-image's
1501/// `while !kawa.out.is_empty()` inside a loop over the scheduler's order — so
1502/// the write side is a DRIVE LOOP. [`ConnectionH2::handle_write`] settles one
1503/// transmit and answers nothing; the pass's result comes from [`Self::Done`]
1504/// or from the `ConnectionH2::finalize_write` the caller runs for
1505/// [`Self::Finalize`].
1506#[derive(Debug, Clone, Copy)]
1507pub enum H2WriteTarget {
1508 /// The core ended the pass by itself. No write, no
1509 /// [`ConnectionH2::handle_write`], and no finalize — this is the pass's
1510 /// result.
1511 ///
1512 /// Two sites, and neither may become a [`Self::Finalize`]: the resume
1513 /// path's stall and the MadeYouReset emitted-RST cap trip. Folding either
1514 /// into `Finalize` would run
1515 /// LIFECYCLE §9 invariant 16's readiness policy over a pass that must not
1516 /// reach it.
1517 Done(MuxResult),
1518 /// Gather `stream_id`'s queued blocks with [`h2_transmit::gather`], hand
1519 /// the descriptors to `socket_write_vectored`, discharge them with
1520 /// [`h2_transmit::confirm`], then answer with
1521 /// [`ConnectionH2::handle_write`].
1522 ///
1523 /// `stream_id` is never [`H2StreamId::Zero`]: `self.zero` is input only,
1524 /// the resume phase parks and matches [`H2StreamId::Other`] only, and the
1525 /// scheduler loop walks real stream ids. The transmit sends the ordered
1526 /// output queue first (`ConnectionH2::gather_transmit`), so the rest of a
1527 /// frame an earlier write cut always leaves ahead of the next frame.
1528 Transmit { stream_id: H2StreamId },
1529 /// The pass is over and owes the readiness decision of LIFECYCLE §9
1530 /// invariant 16. Read `H2Shell::tls_wants_write`, hand it to
1531 /// `ConnectionH2::finalize_write` alongside `socket_write` and
1532 /// `bytes_written`, and drive the `H2FinalizeTarget` that answers; the
1533 /// end of that protocol is the pass's result.
1534 ///
1535 /// `bytes_written` rides along with `socket_write` because
1536 /// `finalize_write` reads it as `made_progress`, and that is what selects
1537 /// `FinalizeAction::RetainPendingBack` over `Quiesce`. It is accumulated
1538 /// stream by stream inside the loop, so only the pass knows it.
1539 Finalize {
1540 socket_write: bool,
1541 bytes_written: usize,
1542 },
1543}
1544
1545/// What one [`H2WriteTarget::Transmit`] write did, split the way
1546/// [`ConnectionH2::handle_write`] settles it.
1547///
1548/// The transmit sends the ordered output queue first, then the stream's
1549/// frames (`h2_transmit::gather_after`), so the socket's count is split at
1550/// the queue's length; and when the write stopped inside a stream frame, the
1551/// rest of that frame is adopted by the queue (#1604).
1552#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1553pub struct H2Written {
1554 /// What the socket took, all of it.
1555 pub size: usize,
1556 /// The part of `size` that came from the output queue. The rest,
1557 /// `size - from_output`, is the stream's, and the caller already
1558 /// confirmed exactly that much on the stream's `kawa`.
1559 pub from_output: usize,
1560 /// The unsent rest of the frame (or header block) the write cut, as
1561 /// `h2_transmit::frame_tail` measured it before the confirm: still at the
1562 /// front of the stream's `kawa.out`, and moved to the output queue by
1563 /// [`ConnectionH2::handle_write`].
1564 pub tail: usize,
1565}
1566
1567/// What [`ConnectionH2::finalize_write`] wants its caller to do before it may
1568/// call [`ConnectionH2::finalize_write_after_flush`].
1569///
1570/// [`H2WriteTarget::Finalize`]'s own two-call protocol, built the way
1571/// [`H2ReadTarget`] builds the read's: the core answers a step, the shell
1572/// performs the socket work, and the core is told what the socket then said.
1573/// The step here is the TLS flush triple — read
1574/// `H2Shell::tls_wants_write`, perform or skip
1575/// `H2Shell::flush_tls_records`, read it again — whose middle is the
1576/// only *action* of the three TLS seams and therefore the one place this pass
1577/// has to invert control rather than take a value in.
1578///
1579/// [`Self::Flush`] and [`Self::SkipFlush`] are two variants rather than one
1580/// boolean because `h2_close::finalize_action` already distinguishes them:
1581/// they differ only in the middle step and share the whole of what follows.
1582#[derive(Debug, Clone, Copy)]
1583pub enum H2FinalizeTarget {
1584 /// The pre-flush decision ended the pass: the RFC 9113 §6.8 graceful
1585 /// GOAWAY, or one of the four readiness answers taken while rustls holds
1586 /// nothing. No flush and no [`ConnectionH2::finalize_write_after_flush`]
1587 /// — this is the pass's result.
1588 Done(MuxResult),
1589 /// rustls holds records and this pass wrote nothing: perform
1590 /// `H2Shell::flush_tls_records`, read
1591 /// `H2Shell::tls_wants_write` again, and answer with
1592 /// [`ConnectionH2::finalize_write_after_flush`].
1593 Flush,
1594 /// rustls holds records but `socket_write_vectored` already attempted this
1595 /// pass's flush as a side effect: read `H2Shell::tls_wants_write`
1596 /// again WITHOUT spending a second empty-buffer write on it, and answer
1597 /// with [`ConnectionH2::finalize_write_after_flush`].
1598 ///
1599 /// That second read is not a repeat of the first even on this branch: the
1600 /// vectored writes the pass already made are what may have changed the
1601 /// answer.
1602 SkipFlush,
1603}
1604
1605/// What [`ConnectionH2::flush_pending_control_frames`] wants its caller to do
1606/// with the pass it just ran a control-frame preamble over.
1607///
1608/// [`Self::Stalled`] is what replaces the TLS re-arm the stalled-drain tails
1609/// used to perform inline. An output flush that stalled has to be followed by
1610/// a FRESH `H2Shell::tls_wants_write` — that write is what changes the
1611/// answer, so no value read before it can stand in — and the shell that owns
1612/// the socket is the layer that reads it.
1613///
1614/// [`Self::Proceed`] is the former `None`: it carries no `MuxResult` because
1615/// the preamble did not end the pass, and `writable()` falls through to its
1616/// `(H2State, Position)` dispatch.
1617#[derive(Debug, Clone, Copy)]
1618pub enum H2ControlFlushTarget {
1619 /// Nothing owed, nothing stalled: `writable()` proceeds into its state
1620 /// dispatch.
1621 Proceed,
1622 /// The preamble ended the pass by itself — a SETTINGS-ACK timeout GOAWAY
1623 /// or an RST_STREAM lifetime-cap GOAWAY. This is the pass's result, and no TLS query
1624 /// follows it.
1625 Done(MuxResult),
1626 /// The output flush stalled. Read `tls_wants_write`, hand it to
1627 /// [`ConnectionH2::ensure_tls_flushed`], and answer the pass with
1628 /// [`MuxResult::Continue`].
1629 Stalled,
1630 /// The connection's ordered output queue holds bytes: the control frames
1631 /// this walk just serialised behind whatever was already queued. Drain
1632 /// [`ConnectionH2::output_pending`] to the socket and answer with
1633 /// [`ConnectionH2::handle_control_flush`].
1634 FlushOutput,
1635}
1636
1637/// What a control-flush walk answers once the
1638/// [`H2ControlFlushTarget::FlushOutput`] it raised has been performed.
1639///
1640/// [`H2ControlFlushTarget`] minus that variant. A separate enum rather than
1641/// the same one, for the reason [`H2WritableStateTarget`] gives against
1642/// reusing [`H2FinalizeTarget`]: `H2Shell::drive_control_flush` consumes every
1643/// `FlushOutput` itself, so sharing the enum would force a named-impossible arm
1644/// into the write pass's match — a reader would have to work out that it
1645/// cannot happen, and a later edit could make it happen without the match
1646/// noticing.
1647#[derive(Debug, Clone, Copy)]
1648enum ControlFlushOutcome {
1649 /// Nothing owed, nothing stalled: the pass proceeds into its state
1650 /// dispatch.
1651 Proceed,
1652 /// The walk ended the pass by itself.
1653 Done(MuxResult),
1654 /// The output flush stalled.
1655 Stalled,
1656}
1657
1658/// What [`ConnectionH2::dispatch_writable_state`] wants its caller to do with
1659/// the `(H2State, Position)` arm it just took.
1660///
1661/// The connection-level write pass's own protocol, built the way
1662/// [`H2FinalizeTarget`] builds the stream-level one: the core answers a step,
1663/// the shell performs the socket work, and the core is told what the socket
1664/// then said. [`Self::Flush`] carries NO answer with it — the post-flush query
1665/// can only enter the core through
1666/// [`ConnectionH2::dispatch_writable_state_after_flush`]'s own parameter,
1667/// which is what stops "does rustls hold records" and "did the kernel take
1668/// them" from ever being read off one binding. `h2_close::TlsFlushPhase`
1669/// states why those are two questions and what conflating them costs.
1670///
1671/// Not [`H2FinalizeTarget`] reused. That enum's `SkipFlush` answers a question
1672/// no close arm asks: nothing writes to the socket between this dispatch and
1673/// its flush, so no vectored write can have attempted it as a side effect, and
1674/// sharing the enum would force a named-impossible arm into the shell's match.
1675/// It is the argument `h2_close` already makes against widening `CloseAction`
1676/// with `FinalizeAction`'s readiness answers, applied the other way round.
1677#[derive(Debug, Clone, Copy)]
1678pub enum H2WritableStateTarget {
1679 /// The arm ended the pass by itself: this is the pass's result. Every arm
1680 /// but the two below answers this, the `H2State::GoAway` arm's
1681 /// departed-peer and nothing-pending answers included.
1682 Done(MuxResult),
1683 /// The `H2State::GoAway` arm asks for the second flush of this pass:
1684 /// perform `H2Shell::flush_tls_records`, read
1685 /// `H2Shell::tls_wants_write` again, and answer with
1686 /// [`ConnectionH2::dispatch_writable_state_after_flush`].
1687 ///
1688 /// The preamble already attempted one flush this pass; this one re-checks
1689 /// because a close is about to be decided on the answer. Both are
1690 /// deliberate — see `h2_close`'s module doc.
1691 Flush,
1692 /// A proxying state: run `H2Shell::write_streams`, whose result is
1693 /// the pass's. It is the caller's to run for the same reason the flush is,
1694 /// and it is why the dispatch takes neither `Context` nor `Endpoint`.
1695 WriteStreams,
1696}
1697
1698/// The half of [`ConnectionH2::force_disconnect`] that still owes the socket a
1699/// question, parked until the shell can ask it.
1700///
1701/// The fourth member of the poll/handle family, and the only one that cannot
1702/// be *returned* by the function that raises it. [`H2FinalizeTarget`],
1703/// [`H2ControlFlushTarget`] and [`H2WritableStateTarget`] are answers a core
1704/// step hands straight back to its caller; this one is raised at seventeen
1705/// sites, seven of them inside [`ConnectionH2::handle_read`], whose `pub`
1706/// signature returns [`MuxResult`] and does not change. A target that cannot
1707/// ride the return value has to be parked on the connection instead, which is
1708/// why this is a field rather than a return type. The pairing is otherwise
1709/// identical: [`ConnectionH2::poll_force_disconnect`] takes it, the shell
1710/// reads [`SocketHandler::socket_wants_write`](crate::socket::SocketHandler::socket_wants_write),
1711/// and [`ConnectionH2::force_disconnect_after_query`] receives that answer as
1712/// its own parameter.
1713///
1714/// Both variants carry everything the settlement needs that the socket cannot
1715/// supply, so nothing is re-derived after the fact. `peer_gone` in particular
1716/// is computed at the raising site and carried: it reads `stream_table` and
1717/// `zero.storage`, and `ConnectionH2::remove_dead_stream` runs between the
1718/// raise and the settlement on the `ConnectionH2::reset_stream` paths, so a
1719/// value re-read at settlement time would not be the one the decision was
1720/// entitled to.
1721///
1722/// `Position::Client` is a variant rather than a settled answer because its
1723/// arm's `debug!` renders `wants_write=` too, and that line must report what
1724/// the socket says rather than a literal — see `ConnectionH2::peer_address`'s
1725/// neighbouring note on the same rule.
1726#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1727pub enum H2ForceDisconnectTarget {
1728 /// A `Position::Client` connection. The settlement logs and answers
1729 /// [`MuxResult::Continue`]; the backend status and the `HUP` readiness
1730 /// event were already written at the raising site, because neither needs
1731 /// the socket.
1732 Client,
1733 /// A `Position::Server` connection, with `peer_gone_after_final_goaway()`
1734 /// as the raising site read it. The settlement runs
1735 /// `h2_close::force_disconnect_action` on the pair and performs the
1736 /// `ReArmAndContinue` readiness write when that is the answer.
1737 Server { peer_gone: bool },
1738}
1739
1740/// The buffer an [`H2WriteTarget::Transmit`] names, resolved to the `Kawa`
1741/// owning it — the exact mirror of [`read_buffer`], and taking `zero` and
1742/// `streams` apart for the same reason: `ConnectionH2::socket` and
1743/// `Context::debug` must stay borrowable while the buffer this returns is
1744/// live, which a method returning a borrow of all of `*self` would forbid.
1745/// The write shell needs precisely that, since it holds the gathered
1746/// descriptors across its `socket_write_vectored`.
1747///
1748/// [`H2StreamId::Zero`] resolves to the connection-level scratch for
1749/// totality; no [`H2WriteTarget::Transmit`] names it.
1750fn write_buffer<'a>(
1751 zero: &'a mut GenericHttpStream,
1752 streams: &'a mut [Stream],
1753 position: &Position,
1754 stream_id: H2StreamId,
1755) -> &'a mut GenericHttpStream {
1756 match stream_id {
1757 H2StreamId::Zero => zero,
1758 H2StreamId::Other {
1759 gid: global_stream_id,
1760 ..
1761 } => streams[global_stream_id].split(position).wbuffer,
1762 }
1763}
1764
1765/// The exact space [`H2ReadTarget::Fill`] offers its caller: `amount` bytes of
1766/// [`read_buffer`]'s free storage, never the whole of it.
1767///
1768/// The cap is load-bearing. `expect_read` is a byte debt for *one* frame, and
1769/// [`ConnectionH2::handle_read`] parses everything the read appended as that
1770/// frame's body — so a caller reading past `amount` would fold the next
1771/// frame's header into this frame's payload.
1772fn read_space<'a>(
1773 zero: &'a mut GenericHttpStream,
1774 streams: &'a mut [Stream],
1775 position: &Position,
1776 stream_id: H2StreamId,
1777 amount: usize,
1778) -> &'a mut [u8] {
1779 &mut read_buffer(zero, streams, position, stream_id)
1780 .storage
1781 .space()[..amount]
1782}
1783
1784/// What [`ConnectionH2::refuse_stream_and_discard`] hands the `H2State::Discard`
1785/// arm of [`ConnectionH2::handle_read`] to locate a refused stream's HPACK field
1786/// block, so the connection decoder can still be advanced before the bytes are
1787/// dropped (RFC 9113 §4.3).
1788///
1789/// RFC 9113 §6.10: only a HEADERS frame carries PADDED/PRIORITY — a
1790/// CONTINUATION never does. `New` therefore keeps the whole flag byte so the
1791/// fragment can be located inside the raw payload the same way
1792/// [`parser::headers_frame`] would. `Continuation` already knows everything
1793/// accumulated from every *prior* frame in the block (copied out eagerly,
1794/// see the call site in `handle_continuation_header_state` for why it cannot
1795/// be a byte offset into `zero.storage` instead) and only needs this frame's
1796/// own END_HEADERS bit.
1797#[derive(Debug)]
1798enum DiscardedFieldBlock {
1799 /// A brand-new stream's initiating HEADERS frame was refused whole
1800 /// (drain, MAX_CONCURRENT_STREAMS, or buffer-pool exhaustion).
1801 New { flags: u8 },
1802 /// A later CONTINUATION frame in an in-progress header block was refused
1803 /// (CVE-2024-27316 flood mitigation). `prior_fragment` is every field-block
1804 /// byte accumulated before this (about to be refused) frame's own payload,
1805 /// which is still unread at capture time and gets appended once it lands
1806 /// in `zero.storage` at Discard time.
1807 Continuation {
1808 prior_fragment: Vec<u8>,
1809 end_headers: bool,
1810 },
1811}
1812
1813/// Decode a refused stream's HPACK field block into `decoder` so the
1814/// connection-level dynamic table stays in sync with the peer's encoder (RFC
1815/// 9113 §4.3) — no header pair is kept, the stream itself was already
1816/// refused and its headers are never used. `payload` is the just-read bytes
1817/// sitting in `zero.storage` at the moment `H2State::Discard` is reached
1818/// (this frame's own payload only — see [`DiscardedFieldBlock::Continuation`]
1819/// for how any earlier frames' bytes are threaded in).
1820///
1821/// A free function rather than a `ConnectionH2` method: the caller in the
1822/// `H2State::Discard` arm of [`ConnectionH2::handle_read`] already holds
1823/// `zero.storage` borrowed as `kawa`, and a method needing the whole
1824/// `&mut self` would conflict with that borrow. Taking `decoder` and
1825/// `payload` as disjoint parameters keeps the borrow legal.
1826fn decode_discarded_field_block(
1827 decoder: &mut crate::protocol::mux::hpack::Decoder,
1828 payload: &[u8],
1829 discarded: DiscardedFieldBlock,
1830) -> Result<Option<Vec<u8>>, H2Error> {
1831 match discarded {
1832 DiscardedFieldBlock::New { flags } => {
1833 // Reparse using the same grammar as an accepted HEADERS frame
1834 // (RFC 9113 §6.2): [Pad Length][Stream Dependency+Weight][field
1835 // block fragment][padding]. `payload` is exactly this frame's
1836 // whole wire payload — `refuse_stream_and_discard` was handed
1837 // `header.payload_len` before any padding/priority stripping —
1838 // so the field block has to be located the same way
1839 // `parser::headers_frame` locates it for an accepted stream.
1840 let header = FrameHeader {
1841 payload_len: payload.len() as u32,
1842 frame_type: FrameType::Headers,
1843 flags,
1844 stream_id: 0,
1845 };
1846 let (_, frame) =
1847 parser::headers_frame(payload, &header).map_err(|_| H2Error::ProtocolError)?;
1848 let Frame::Headers(headers) = frame else {
1849 unreachable!("parser::headers_frame always yields Frame::Headers")
1850 };
1851 let fragment = headers
1852 .header_block_fragment
1853 .data_opt(payload)
1854 .ok_or(H2Error::InternalError)?;
1855 // Without END_HEADERS the block continues on CONTINUATION frames
1856 // (RFC 9113 §6.10): hand its fragment back, to be completed by
1857 // them (`ConnectionH2::pending_discarded_block`) and decoded
1858 // whole, since a fragment may end mid-integer or
1859 // mid-Huffman-string.
1860 if !headers.end_headers {
1861 return Ok(Some(fragment.to_vec()));
1862 }
1863 decoder
1864 .decode_with_cb(fragment, |_, _| {})
1865 .map(|_| None)
1866 .map_err(|_| H2Error::CompressionError)
1867 }
1868 DiscardedFieldBlock::Continuation {
1869 mut prior_fragment,
1870 end_headers,
1871 } => {
1872 // Same END_HEADERS gate as above, evaluated on this trailing
1873 // CONTINUATION frame's own flags rather than the (still false)
1874 // flag the originating HEADERS frame carried.
1875 prior_fragment.extend_from_slice(payload);
1876 if !end_headers {
1877 return Ok(Some(prior_fragment));
1878 }
1879 decoder
1880 .decode_with_cb(&prior_fragment, |_, _| {})
1881 .map(|_| None)
1882 .map_err(|_| H2Error::CompressionError)
1883 }
1884 }
1885}
1886
1887impl ConnectionH2 {
1888 fn frontend_hung_up_while_draining(&self) -> bool {
1889 matches!(self.position, Position::Server)
1890 && self.drain.draining()
1891 && (self.readiness.event.is_hup() || self.readiness.event.is_error())
1892 }
1893
1894 /// Once the final GOAWAY has been queued and all streams/control frames are
1895 /// gone, a peer-side HUP/ERR means any remaining rustls backlog is no
1896 /// longer deliverable. Waiting on `socket_wants_write()` in that state can
1897 /// deadlock shutdown forever because GOAWAY disables further frame reads.
1898 fn peer_gone_after_final_goaway(&self) -> bool {
1899 self.frontend_hung_up_while_draining()
1900 && matches!(self.state, H2State::GoAway | H2State::Error)
1901 && self.stream_table.streams().is_empty()
1902 && self.stream_table.expect_write().is_none()
1903 && self.output.is_empty()
1904 }
1905
1906 /// The next instant this connection wants its embedder to call `timeout()`
1907 /// at, or `None` for "no timer". The adapter reflects this onto the real
1908 /// wheel; nothing in this module touches `crate::timer`.
1909 pub fn poll_timeout(&self) -> Option<Instant> {
1910 self.timeout_deadline
1911 }
1912
1913 /// Push the deadline one full [`Self::timeout_duration`] out from the
1914 /// connection's clock snapshot. Replaces the old
1915 /// `TimeoutContainer::reset()` at the same three call sites, and reads
1916 /// `self.now` rather than the real clock (invariant 20).
1917 ///
1918 /// A no-op while lingering: the linger deadline bounds the whole drain.
1919 pub fn arm_timeout(&mut self) {
1920 if self.is_lingering() {
1921 return;
1922 }
1923 self.timeout_deadline = self.now.checked_add(self.timeout_duration);
1924 }
1925
1926 /// Ask for no timer at all. Replaces `TimeoutContainer::cancel()`.
1927 pub(super) fn clear_timeout(&mut self) {
1928 self.timeout_deadline = None;
1929 }
1930
1931 /// Adopt a new configured duration and re-arm from `now`.
1932 ///
1933 /// Mirrors `TimeoutContainer::set_duration`, which likewise cancelled and
1934 /// re-armed rather than keeping the old deadline. `now` is the adapter's
1935 /// snapshot: this is called from `Mux::ready`, outside the entry points
1936 /// that mirror it into `self.now`.
1937 pub fn set_timeout_duration(&mut self, duration: Duration, now: Instant) {
1938 self.timeout_duration = duration;
1939 if !self.is_lingering() {
1940 self.timeout_deadline = now.checked_add(duration);
1941 }
1942 }
1943
1944 /// Whether the connection is draining what its client still sends after
1945 /// its final GOAWAY (see [`super::shared::Linger`]).
1946 pub(super) fn is_lingering(&self) -> bool {
1947 matches!(self.linger, Linger::Draining { .. })
1948 }
1949
1950 /// Whether a close the next writable pass decides should linger instead:
1951 /// a frontend that has queued its final GOAWAY, whose client has not hung
1952 /// up, and which does not drain already — or one whose linger is already
1953 /// [`Linger::Pending`] behind a TLS `close_notify`. The client may still
1954 /// be sending frames it wrote before it read the GOAWAY (RFC 9113 §6.8);
1955 /// closing with them unread, or receiving more, makes the kernel reset
1956 /// the connection and discard the bytes still queued, the GOAWAY itself
1957 /// included (sozu-proxy/sozu#1861). Whatever the GOAWAY's error code;
1958 /// [`Self::linger_budget`] decides how long and how much.
1959 fn lingers_instead_of_closing(&self) -> bool {
1960 if matches!(self.linger, Linger::Pending { .. }) {
1961 return true;
1962 }
1963 self.position.is_server()
1964 && matches!(self.state, H2State::GoAway)
1965 && self.drain.draining()
1966 && !self.frontend_hung_up_while_draining()
1967 && matches!(self.linger, Linger::Off)
1968 }
1969
1970 /// The lingering close's wall-clock duration and byte budget. After a
1971 /// final GOAWAY(NO_ERROR) the client may still be sending a request it is
1972 /// owed an answer to, so the linger takes the listener's
1973 /// `request_timeout` and [`LINGER_MAX_BYTES`]. After a GOAWAY carrying an
1974 /// error code it only has to deliver that GOAWAY:
1975 /// [`ERROR_GOAWAY_LINGER_TIMEOUT`] (never more than `request_timeout`)
1976 /// and [`ERROR_GOAWAY_LINGER_MAX_BYTES`].
1977 fn linger_budget(&self) -> (Duration, usize) {
1978 let budget = if self.graceful_goaway {
1979 (self.linger_timeout, LINGER_MAX_BYTES)
1980 } else {
1981 (
1982 self.linger_timeout.min(ERROR_GOAWAY_LINGER_TIMEOUT),
1983 ERROR_GOAWAY_LINGER_MAX_BYTES,
1984 )
1985 };
1986 debug_assert!(
1987 budget.0 <= self.linger_timeout && budget.1 <= LINGER_MAX_BYTES,
1988 "no linger outlasts or outreads the graceful one"
1989 );
1990 budget
1991 }
1992
1993 /// Shared constructor for both server and client H2 connections.
1994 ///
1995 /// Differences between server and client are captured by the caller-provided
1996 /// `position`, `expect_read`, and `readiness_interest` parameters.
1997 #[allow(clippy::too_many_arguments)]
1998 pub fn new(
1999 session_ulid: Ulid,
2000 peer_address: Option<std::net::SocketAddr>,
2001 position: super::Position,
2002 buffers: &mut dyn super::buffer_source::BufferSource,
2003 flood_config: H2FloodConfig,
2004 connection_config: H2ConnectionConfig,
2005 stream_idle_timeout: std::time::Duration,
2006 graceful_shutdown_deadline: Option<std::time::Duration>,
2007 timeout_duration: std::time::Duration,
2008 expect_read: Option<(H2StreamId, usize)>,
2009 readiness_interest: sozu_command::ready::Ready,
2010 ) -> Option<Self> {
2011 let buffer = buffers.checkout()?;
2012 // The one clock sample this module takes outside `Mux`'s sampling
2013 // points. A connection is constructed at accept / backend-connect
2014 // time, outside any `ready()` pass, so there is no snapshot to
2015 // inherit; everything downstream of here reads `self.now`, which
2016 // `Mux` refreshes from its next pass onward.
2017 let now = Instant::now();
2018 let local_settings = H2Settings {
2019 settings_max_concurrent_streams: connection_config.max_concurrent_streams,
2020 ..H2Settings::default()
2021 };
2022 // RFC 7541 §4.2: enforce SETTINGS_HEADER_TABLE_SIZE as the upper bound
2023 // for dynamic table size updates from the peer
2024 let hpack = hpack_state::HpackState::new(
2025 buffers,
2026 local_settings.settings_header_table_size as usize,
2027 )?;
2028 Some(ConnectionH2 {
2029 session_ulid,
2030 hpack,
2031 stream_table: h2_stream_table::H2StreamTable::new(expect_read),
2032 last_stream_id: 0,
2033 local_settings,
2034 peer_settings: H2Settings::default(),
2035 position,
2036 scheduler: h2_scheduler::H2Scheduler::default(),
2037 readiness: crate::Readiness {
2038 interest: readiness_interest,
2039 event: Ready::EMPTY,
2040 },
2041 peer_address,
2042 state: H2State::ClientPreface,
2043 timeout_duration,
2044 // Armed from construction, exactly as the old `TimeoutContainer`
2045 // was: the frontend arrived already armed from the handshake state,
2046 // and `Router::plan_connect` armed a fresh backend with the connect
2047 // timeout right after registering its socket. The adapter reflects
2048 // this onto the wheel on its next reschedule.
2049 timeout_deadline: now.checked_add(timeout_duration),
2050 linger_timeout: timeout_duration,
2051 linger: Linger::Off,
2052 graceful_goaway: false,
2053 flow_control: h2_flow_control::H2FlowControl::new(DEFAULT_INITIAL_WINDOW_SIZE as i32),
2054 pending_table_size_update: None,
2055 parked_header_block: false,
2056 parked_data: 0,
2057 connection_window_enlarged: false,
2058 drain: h2_drain::H2DrainState::new(graceful_shutdown_deadline),
2059 zero: kawa::Kawa::new(kawa::Kind::Request, kawa::Buffer::new(buffer)),
2060 output: h2_output::H2Output::default(),
2061 reads_wait_for_output: false,
2062 header_reassembly: h2_header_reassembly::HeaderBlockAccumulator::new(),
2063 bytes: H2ByteAccounting {
2064 zero_bytes_read: 0,
2065 overhead_bin: 0,
2066 overhead_bout: 0,
2067 },
2068 flood_detector: h2_flood_detector::H2FloodDetector::new(flood_config, now),
2069 settings_sent_at: None,
2070 control_tx: h2_control_tx::H2ControlTx::new(connection_config.max_concurrent_streams),
2071 discarded_field_block: None,
2072 pending_discarded_block: None,
2073 close_notify_sent: false,
2074 kernel_refused_write: false,
2075 max_pending_window_updates: 1 + connection_config.max_concurrent_streams as usize * 4,
2076 connection_config,
2077 ready_incremental_streams: 0,
2078 last_gauge_snapshot: None,
2079 metric_events: Vec::new(),
2080 stream_idle_timeout,
2081 refuse_count_window: 0,
2082 refuse_window_start: now,
2083 mcs_backpressure_applied: false,
2084 flood_refused_streams: None,
2085 flood_refusal_ignored: false,
2086 now,
2087 pending_force_disconnect: None,
2088 })
2089 }
2090
2091 fn expect_header(&mut self) {
2092 self.state = H2State::Header;
2093 self.stream_table
2094 .set_expect_read(Some((H2StreamId::Zero, 9)));
2095 }
2096
2097 /// Process the `H2State::Header` state: parse a 9-byte frame header from
2098 /// `self.zero`, validate the stream, create new streams if needed, and
2099 /// transition to `H2State::Frame` for the payload.
2100 ///
2101 /// Returns `MuxResult` — the caller should propagate the result directly.
2102 fn handle_header_state<E, L>(&mut self, context: &mut Context<L>, endpoint: E) -> MuxResult
2103 where
2104 E: Endpoint,
2105 L: ListenerHandler + L7ListenerHandler,
2106 {
2107 let i = self.zero.storage.data();
2108 trace!("{} header: {:?}", log_context!(self), i);
2109 match parser::frame_header(i, self.local_settings.settings_max_frame_size) {
2110 Ok((_, header)) => {
2111 trace!("{} {:#?}", log_context!(self), header);
2112 self.zero.storage.clear();
2113 let stream_id = header.stream_id;
2114 // A discarded header block without END_HEADERS continues
2115 // here: only a CONTINUATION frame on its stream may follow
2116 // (RFC 9113 §6.10), and it is discarded the same way, its
2117 // fragment completing the block for HPACK (§4.3).
2118 if let Some((block_stream_id, fragment)) = self.pending_discarded_block.take() {
2119 if header.frame_type != FrameType::Continuation || stream_id != block_stream_id
2120 {
2121 error!(
2122 "{} {:?} on stream {} interrupts the header block of stream {}",
2123 log_context!(self),
2124 header.frame_type,
2125 stream_id,
2126 block_stream_id
2127 );
2128 return self.goaway(H2Error::ProtocolError);
2129 }
2130 // CVE-2024-27316: the same per-block CONTINUATION count and
2131 // size limits as an accepted block.
2132 self.flood_detector
2133 .record_continuation_frame(header.payload_len);
2134 check_flood_or_return!(self);
2135 if self.flood_detector.accumulated_header_size()
2136 > self.flood_detector.config().max_header_list_size()
2137 || (header.payload_len as usize) > self.zero.storage.available_space()
2138 {
2139 error!(
2140 "{} discarded header block of stream {} exceeds its limits",
2141 log_context!(self),
2142 stream_id
2143 );
2144 return self.goaway(H2Error::EnhanceYourCalm);
2145 }
2146 return self.discard_field_block(
2147 stream_id,
2148 header.payload_len,
2149 DiscardedFieldBlock::Continuation {
2150 prior_fragment: fragment,
2151 end_headers: header.flags & parser::FLAG_END_HEADERS != 0,
2152 },
2153 );
2154 }
2155 // RFC 9113 §6.10: CONTINUATION frames MUST be preceded by a
2156 // HEADERS or PUSH_PROMISE frame without END_HEADERS. When we
2157 // reach `handle_header_state`, we are between frames and no
2158 // header block is in progress (otherwise the state would be
2159 // `H2State::ContinuationHeader`). A CONTINUATION frame arriving
2160 // here is therefore standalone and MUST be treated as a
2161 // connection error of type PROTOCOL_ERROR.
2162 if header.frame_type == FrameType::Continuation {
2163 error!(
2164 "{} standalone CONTINUATION frame on stream {} without preceding HEADERS",
2165 log_context!(self),
2166 stream_id
2167 );
2168 return self.goaway(H2Error::ProtocolError);
2169 }
2170 // RFC 9113 §5.5: unknown frame types MUST be ignored and discarded.
2171 // Route unknown frames (and any stream_id == 0 control frame)
2172 // through stream 0 (the connection-level buffer) so
2173 // `handle_frame` can drop them without touching stream state.
2174 let read_stream = if stream_id == 0
2175 || matches!(header.frame_type, FrameType::Unknown(_))
2176 {
2177 H2StreamId::Zero
2178 } else if (header.frame_type == FrameType::Data
2179 || header.frame_type == FrameType::Headers)
2180 && self.stream_table.was_reset_locally(stream_id)
2181 {
2182 // RFC 9113 §5.1: after sending RST_STREAM, an endpoint
2183 // MUST ignore frames it receives on that stream, which
2184 // the peer may have sent before reading the reset,
2185 // whether the stream is still tracked or already retired.
2186 // `discard_frame_payload` still processes them minimally:
2187 // a HEADERS block, and the CONTINUATION frames completing
2188 // it, are decoded so the HPACK dynamic table stays in
2189 // sync (§4.3); a DATA payload is credited to connection
2190 // flow control (§6.9). Every other frame type on such a
2191 // stream keeps its own handling.
2192 debug!(
2193 "{} Ignoring {:?} on stream {} after sending RST_STREAM",
2194 log_context!(self),
2195 header.frame_type,
2196 stream_id
2197 );
2198 // What the peer had in flight when it read the reset is
2199 // no abuse: at most two header blocks (a trailer section,
2200 // or an interim and a final response), or DATA within the
2201 // stream's receive window, which Sōzu credits back as it
2202 // reads DATA, so it stays the initial window it
2203 // advertised. More header blocks, and DATA beyond it,
2204 // count as a glitch.
2205 // CVE-2019-9518: an empty DATA frame without END_STREAM
2206 // counts toward the empty-DATA flood limit here as on a
2207 // live stream (`handle_data_frame`), since it carries no
2208 // payload to charge against the stream's window.
2209 if header.frame_type == FrameType::Data
2210 && header.payload_len == 0
2211 && header.flags & parser::FLAG_END_STREAM == 0
2212 {
2213 self.flood_detector.record_empty_data_frame();
2214 check_flood_or_return!(self);
2215 }
2216 let in_flight = if header.frame_type == FrameType::Headers {
2217 self.stream_table
2218 .charge_reset_stream_header_block(stream_id)
2219 } else {
2220 self.stream_table.charge_reset_stream_data(
2221 stream_id,
2222 header.payload_len,
2223 self.local_settings.settings_initial_window_size,
2224 )
2225 };
2226 if !in_flight {
2227 self.flood_detector.record_glitch();
2228 check_flood_or_return!(self);
2229 }
2230 return self.discard_frame_payload(&header);
2231 } else if let Some(global_stream_id) =
2232 self.stream_table.streams().get(&stream_id).copied()
2233 {
2234 let allowed_on_half_closed = header.frame_type == FrameType::WindowUpdate
2235 || header.frame_type == FrameType::Priority
2236 || header.frame_type == FrameType::RstStream;
2237 let carries_message = header.frame_type == FrameType::Data
2238 || header.frame_type == FrameType::Headers;
2239 let stream = &context.streams[global_stream_id];
2240 // Use the position-aware end_of_stream flag:
2241 // - Server reads from front (client requests)
2242 // - Client reads from back (backend responses)
2243 let received_eos = if self.position.is_server() {
2244 stream.front_received_end_of_stream
2245 } else {
2246 stream.back_received_end_of_stream
2247 };
2248 trace!(
2249 "{} REQUESTING EXISTING STREAM {}: {}/{:?}",
2250 log_context!(self),
2251 stream_id,
2252 received_eos,
2253 stream.state
2254 );
2255 // RFC 9113 §5.1: DATA or HEADERS on a half-closed (remote)
2256 // stream is a stream error of type STREAM_CLOSED. Only that
2257 // stream is reset; the frame is still minimally processed.
2258 if carries_message && received_eos && stream.state.is_open() {
2259 debug!(
2260 "{} {:?} on half-closed (remote) stream {}, sending RST_STREAM(STREAM_CLOSED)",
2261 log_context!(self),
2262 header.frame_type,
2263 stream_id
2264 );
2265 return self.reset_stream_and_discard_frame(
2266 &header,
2267 global_stream_id,
2268 H2Error::StreamClosed,
2269 context,
2270 endpoint,
2271 );
2272 }
2273 if !allowed_on_half_closed && (received_eos || !stream.state.is_open()) {
2274 error!(
2275 "{} CANNOT RECEIVE {:?} ON THIS STREAM {:?}",
2276 log_context!(self),
2277 header.frame_type,
2278 stream.state
2279 );
2280 return self.goaway(H2Error::StreamClosed);
2281 }
2282 // RFC 9113 §8.1: a HEADERS frame received in the body
2283 // phase is a trailer block and MUST carry END_STREAM. This
2284 // closes the request-smuggling primitive where a peer sends
2285 // HEADERS, DATA, HEADERS (no END_STREAM) to chain header
2286 // blocks on the same stream ID.
2287 //
2288 // Discriminate from the read-side Kawa parsing phase rather
2289 // than stream existence: on Position::Client the stream is
2290 // created when we send the request to the backend, so the
2291 // initial backend response HEADERS legitimately arrives on
2292 // an existing stream. Similarly, 1xx→final transitions on
2293 // either side may yield multiple HEADERS frames before the
2294 // body begins (kawa clears back to initial / terminated on
2295 // 1xx; neither is main_phase). Only HEADERS arriving once
2296 // the read side has transitioned to Body/Chunks parsing —
2297 // i.e. after headers were fully consumed and body framing
2298 // is in progress — may be a trailer.
2299 let read_in_body = if self.position.is_server() {
2300 stream.front.is_main_phase()
2301 } else {
2302 stream.back.is_main_phase()
2303 };
2304 //
2305 // RFC 9113 §8.1.1: such a message is malformed, a stream
2306 // error of type PROTOCOL_ERROR: only this stream is reset.
2307 if header.frame_type == FrameType::Headers
2308 && read_in_body
2309 && header.flags & parser::FLAG_END_STREAM == 0
2310 {
2311 debug!(
2312 "{} HEADERS without END_STREAM on open stream {} in body phase: trailers MUST carry END_STREAM",
2313 log_context!(self),
2314 stream_id
2315 );
2316 return self.reset_stream_and_discard_frame(
2317 &header,
2318 global_stream_id,
2319 H2Error::ProtocolError,
2320 context,
2321 endpoint,
2322 );
2323 }
2324 if header.frame_type == FrameType::Data {
2325 H2StreamId::Other {
2326 id: stream_id,
2327 gid: global_stream_id,
2328 }
2329 } else {
2330 H2StreamId::Zero
2331 }
2332 } else {
2333 // RFC 9113 §5.1.1: stream identifiers MUST be strictly
2334 // increasing. Tightened from `>=` to `>` so that a peer
2335 // cannot re-use `self.last_stream_id` (which would
2336 // conflict with our own server-pushed streams if we
2337 // ever enable push in the future). For the first
2338 // request on a fresh connection `last_stream_id == 0`
2339 // and any client-initiated odd stream still passes.
2340 // A refused stream moves `highest_peer_stream_id` but
2341 // not `last_stream_id`, which only an accepted stream
2342 // moves, so a new stream must be past both.
2343 let opens_new_client_stream = header.frame_type == FrameType::Headers
2344 && self.position.is_server()
2345 && stream_id & 1 == 1
2346 && stream_id > self.last_stream_id;
2347 if opens_new_client_stream
2348 && stream_id <= self.stream_table.highest_peer_stream_id()
2349 {
2350 // An id between the last accepted stream and the
2351 // highest one the client used was refused or skipped,
2352 // never opened: it is no new stream. Streams still
2353 // tracked, and streams Sōzu reset or refused
2354 // (`H2StreamTable::was_reset_locally`), were handled
2355 // above: the request trailers of a refused stream,
2356 // sent before the client read its REFUSED_STREAM, are
2357 // ignored there (RFC 9113 §5.1). What lands here is an
2358 // id the client skipped, or a refused one the bounded
2359 // recently-reset set already forgot, and HEADERS on it
2360 // is a connection error of type PROTOCOL_ERROR
2361 // (§5.1.1).
2362 error!(
2363 "{} HEADERS on stream {} at or below the highest client stream id {}, sending GOAWAY(PROTOCOL_ERROR)",
2364 log_context!(self),
2365 stream_id,
2366 self.stream_table.highest_peer_stream_id()
2367 );
2368 return self.goaway(H2Error::ProtocolError);
2369 }
2370 if opens_new_client_stream {
2371 // RFC 9113 §6.8: after sending a GOAWAY, the proxy
2372 // MUST NOT accept new streams.
2373 // `graceful_goaway` marks the connection draining
2374 // through `H2DrainState::begin_graceful_drain`, then
2375 // sends an initial GOAWAY with last_stream_id =
2376 // STREAM_ID_MAX (so in-flight requests are still
2377 // accepted); *new* peer streams must still be refused.
2378 // Without this check, a peer racing the drain
2379 // window could open arbitrary new streams between
2380 // the initial and final GOAWAY emission.
2381 if self.drain.draining() {
2382 // A refused stream costs a glitch, as below.
2383 if self.settings_sent_at.is_none() {
2384 self.flood_detector.record_glitch();
2385 check_flood_or_return!(self);
2386 }
2387 self.stream_table.observe_peer_stream_id(stream_id);
2388 return self.refuse_stream_and_discard(
2389 stream_id,
2390 H2Error::RefusedStream,
2391 RstOrigin::Local,
2392 header.payload_len,
2393 DiscardedFieldBlock::New {
2394 flags: header.flags,
2395 },
2396 );
2397 }
2398 // Soft state below the pre-response RST_STREAM cap:
2399 // refuse the new stream, keep serving the open ones.
2400 // The refusal is retryable (RFC 9113 §8.7). Like the
2401 // refusals here and below, it costs a glitch once the
2402 // peer acknowledged our SETTINGS: the glitch budget is
2403 // what bounds a peer that keeps opening streams it is
2404 // refused. It feeds no other flood counter —
2405 // `RstOrigin::Local`, and no SETTINGS back-pressure,
2406 // which would halve MAX_CONCURRENT_STREAMS for the
2407 // rest of the connection over a state that decays. A
2408 // client that reset a refused stream gets no more
2409 // refusals (`Self::flood_refusal_ignored`).
2410 if !self.flood_refusal_ignored
2411 && self.flood_detector.refuses_new_streams(self.now)
2412 {
2413 if self.settings_sent_at.is_none() {
2414 self.flood_detector.record_glitch();
2415 check_flood_or_return!(self);
2416 }
2417 debug!(
2418 "{} refusing stream {} under flood pressure",
2419 log_context!(self),
2420 stream_id
2421 );
2422 self.metric_events
2423 .push(MetricEvent::StreamRefusedUnderFlood);
2424 self.stream_table.observe_peer_stream_id(stream_id);
2425 self.note_flood_refused_stream(stream_id);
2426 if let Some(result) = self.enqueue_rst(
2427 stream_id,
2428 H2Error::RefusedStream,
2429 RstOrigin::Local,
2430 ) {
2431 return result;
2432 }
2433 self.stream_table.remember_refused(stream_id);
2434 return self.discard_field_block(
2435 stream_id,
2436 header.payload_len,
2437 DiscardedFieldBlock::New {
2438 flags: header.flags,
2439 },
2440 );
2441 }
2442 if self.stream_table.len()
2443 >= self.local_settings.settings_max_concurrent_streams as usize
2444 {
2445 error!(
2446 "{} MAX CONCURRENT STREAMS: limit={}, current={}",
2447 log_context!(self),
2448 self.local_settings.settings_max_concurrent_streams,
2449 self.stream_table.len()
2450 );
2451 // Every refusal counts as a glitch once the peer
2452 // has acknowledged our SETTINGS (before that it
2453 // may legitimately exceed a limit it has not
2454 // seen, RFC 9113 §6.5.3). This includes the band
2455 // back-pressure opens below the advertised limit:
2456 // the glitch budget, not the refusal, is what
2457 // bounds a peer that keeps opening streams it is
2458 // refused. The reset itself stays
2459 // `RstOrigin::Local` and does not feed the
2460 // MadeYouReset cap.
2461 if self.settings_sent_at.is_none() {
2462 self.flood_detector.record_glitch();
2463 check_flood_or_return!(self);
2464 }
2465 // RFC 9113 §6.8: update highest_peer_stream_id BEFORE
2466 // queueing RST_STREAM so GOAWAY reports the correct
2467 // last_stream_id if the connection closes later.
2468 self.stream_table.observe_peer_stream_id(stream_id);
2469 return self.refuse_stream_and_discard(
2470 stream_id,
2471 H2Error::RefusedStream,
2472 RstOrigin::Local,
2473 header.payload_len,
2474 DiscardedFieldBlock::New {
2475 flags: header.flags,
2476 },
2477 );
2478 }
2479 match self.create_stream(stream_id, context) {
2480 Some(_) => {}
2481 None => {
2482 // Buffer pool exhaustion is transient — refuse
2483 // this stream but keep the connection alive so
2484 // existing streams can complete and free buffers.
2485 error!(
2486 "{} Could not create stream {}: buffer pool exhausted",
2487 log_context!(self),
2488 stream_id
2489 );
2490 // RFC 9113 §6.8: update highest_peer_stream_id BEFORE
2491 // queueing RST_STREAM so GOAWAY reports the correct
2492 // last_stream_id if the connection closes later.
2493 self.stream_table.observe_peer_stream_id(stream_id);
2494 return self.refuse_stream_and_discard(
2495 stream_id,
2496 H2Error::RefusedStream,
2497 RstOrigin::Local,
2498 header.payload_len,
2499 DiscardedFieldBlock::New {
2500 flags: header.flags,
2501 },
2502 );
2503 }
2504 }
2505 } else if header.frame_type != FrameType::Priority {
2506 // Distinguish closed vs idle: check whether the stream
2507 // was previously opened. For Server position, compare
2508 // against highest_peer_stream_id (client-initiated).
2509 // For Client position, compare against last_stream_id
2510 // (our own initiated streams) since the peer never
2511 // initiates streams on a backend connection.
2512 let is_closed_stream = if self.position.is_server() {
2513 header.stream_id <= self.stream_table.highest_peer_stream_id()
2514 } else {
2515 header.stream_id < self.last_stream_id
2516 };
2517 if is_closed_stream {
2518 match header.frame_type {
2519 FrameType::RstStream | FrameType::WindowUpdate => {
2520 // RFC 9113 §5.1: RST_STREAM and WINDOW_UPDATE
2521 // on a closed stream can arrive due to race
2522 // conditions and should be consumed/discarded.
2523 debug!(
2524 "{} Ignoring {:?} on closed stream {}",
2525 log_context!(self),
2526 header.frame_type,
2527 header.stream_id
2528 );
2529 self.flood_detector.record_glitch();
2530 check_flood_or_return!(self);
2531 }
2532 FrameType::Data => {
2533 // RFC 9113 §5.1: DATA on a closed stream is a
2534 // stream error of type STREAM_CLOSED. Queue
2535 // RST_STREAM (not GOAWAY) to preserve the
2536 // connection for other streams. The payload is
2537 // still routed through stream 0 so handle_frame
2538 // can do connection-level flow control accounting.
2539 // A stream this endpoint reset or refused never
2540 // gets here while the bounded recently-reset set
2541 // remembers it: its late DATA is ignored above.
2542 debug!(
2543 "{} DATA on closed stream {}, sending RST_STREAM(STREAM_CLOSED)",
2544 log_context!(self),
2545 header.stream_id
2546 );
2547 self.flood_detector.record_glitch();
2548 check_flood_or_return!(self);
2549 // Local: this DATA is already counted as a
2550 // glitch above, which is what bounds it;
2551 // charging it to the provoked-RST cap too
2552 // would count it twice.
2553 if let Some(result) = self.enqueue_rst(
2554 header.stream_id,
2555 H2Error::StreamClosed,
2556 RstOrigin::Local,
2557 ) {
2558 return result;
2559 }
2560 }
2561 _ => {
2562 // RFC 9113 §5.1: HEADERS or other frames on a
2563 // closed stream we did not reset → connection
2564 // error STREAM_CLOSED, which §5.1 permits.
2565 error!(
2566 "{} Received {:?} on closed stream {}, sending GOAWAY(STREAM_CLOSED)",
2567 log_context!(self),
2568 header.frame_type,
2569 header.stream_id
2570 );
2571 return self.goaway(H2Error::StreamClosed);
2572 }
2573 }
2574 } else {
2575 error!(
2576 "{} Received {:?} on idle stream {}, sending GOAWAY(PROTOCOL_ERROR)",
2577 log_context!(self),
2578 header.frame_type,
2579 header.stream_id
2580 );
2581 return self.goaway(H2Error::ProtocolError);
2582 }
2583 }
2584 H2StreamId::Zero
2585 };
2586 trace!(
2587 "{} {} {:?} {:#?}",
2588 log_context!(self),
2589 header.stream_id,
2590 stream_id,
2591 self.stream_table.streams()
2592 );
2593 self.stream_table
2594 .set_expect_read(Some((read_stream, header.payload_len as usize)));
2595 self.state = H2State::Frame(header);
2596 }
2597 Err(error) => {
2598 let error = error_nom_to_h2(error);
2599 error!("{} COULD NOT PARSE FRAME HEADER", log_context!(self));
2600 return self.goaway(error);
2601 }
2602 };
2603 MuxResult::Continue
2604 }
2605
2606 /// Process the `H2State::ContinuationHeader` state: parse a CONTINUATION
2607 /// frame header from `self.zero`, validate stream ID continuity, track
2608 /// flood detection counters, and transition to `ContinuationFrame`.
2609 ///
2610 /// The `headers` parameter is the accumulated HEADERS context from the
2611 /// initial HEADERS frame (cloned from the state enum to avoid borrow
2612 /// conflicts).
2613 fn handle_continuation_header_state(&mut self, headers: &Headers) -> MuxResult {
2614 let i = self.zero.storage.unparsed_data();
2615 trace!("{} continuation header: {:?}", log_context!(self), i);
2616 match parser::frame_header(i, self.local_settings.settings_max_frame_size) {
2617 Ok((
2618 _,
2619 FrameHeader {
2620 payload_len,
2621 frame_type: FrameType::Continuation,
2622 flags,
2623 stream_id,
2624 },
2625 )) => {
2626 // The 9-byte CONTINUATION frame header has now been fully
2627 // parsed from `zero.storage`; nothing about it needs to
2628 // survive the transition below. Earlier revisions kept the
2629 // buffer's trailing bytes alive with a manual
2630 // `storage.end -= 9` so the next payload read would land
2631 // contiguously after the accumulated fragment inside
2632 // `zero.storage` itself — that trick existed only to serve
2633 // the old design where the fragment lived here. It now
2634 // lives in `self.header_reassembly` (h2_header_reassembly.rs),
2635 // so there is nothing left to stay contiguous with.
2636 self.zero.storage.clear();
2637 if stream_id != headers.stream_id {
2638 error!(
2639 "{} CONTINUATION stream_id {} does not match HEADERS stream_id {}",
2640 log_context!(self),
2641 stream_id,
2642 headers.stream_id
2643 );
2644 return self.goaway(H2Error::ProtocolError);
2645 }
2646 // CVE-2024-27316: track CONTINUATION frame count and accumulated size
2647 self.flood_detector.record_continuation_frame(payload_len);
2648 check_flood_or_return!(self);
2649 // RFC 9113 §10.5.1: reject header blocks that cannot be
2650 // buffered. Previously we silently removed READABLE interest
2651 // when amount > available_space, stalling the connection.
2652 // If the payload still fits in our zero buffer we can refuse
2653 // just this stream (RST_STREAM + drain); if not, the
2654 // connection can no longer decode header blocks safely and we
2655 // escalate to GOAWAY(EnhanceYourCalm).
2656 if self.flood_detector.accumulated_header_size()
2657 > self.flood_detector.config().max_header_list_size()
2658 {
2659 error!(
2660 "{} CONTINUATION accumulated header size {} exceeds {}",
2661 log_context!(self),
2662 self.flood_detector.accumulated_header_size(),
2663 self.flood_detector.config().max_header_list_size()
2664 );
2665 if (payload_len as usize) > self.zero.storage.available_space() {
2666 return self.goaway(H2Error::EnhanceYourCalm);
2667 }
2668 // Remove the already-created stream slot before refusing,
2669 // so it does not leak against MAX_CONCURRENT_STREAMS. Route
2670 // through `remove_dead_stream` so the expect_write/read
2671 // invariant (§LIFECYCLE.md 5.4) holds on this path too.
2672 if let Some(global_stream_id) = self.stream_table.get(stream_id) {
2673 self.remove_dead_stream(stream_id, global_stream_id);
2674 }
2675 // The field-block bytes accumulated by every *prior*
2676 // frame in this block already live in
2677 // `self.header_reassembly`, decoupled from
2678 // `zero.storage`'s own bookkeeping — no eager copy is
2679 // needed here the way the old single-buffer design
2680 // required (the buffer this comment used to warn about
2681 // reuse of is `zero.storage`; that reuse now targets an
2682 // empty, unrelated scratch region, not this fragment).
2683 let prior_fragment = self.header_reassembly.finish();
2684 return self.refuse_stream_and_discard(
2685 stream_id,
2686 H2Error::RefusedStream,
2687 RstOrigin::PeerProvoked,
2688 payload_len,
2689 DiscardedFieldBlock::Continuation {
2690 prior_fragment,
2691 end_headers: flags & parser::FLAG_END_HEADERS != 0,
2692 },
2693 );
2694 }
2695 if (payload_len as usize) > self.zero.storage.available_space() {
2696 error!(
2697 "{} CONTINUATION payload {} exceeds buffer space {}",
2698 log_context!(self),
2699 payload_len,
2700 self.zero.storage.available_space()
2701 );
2702 return self.goaway(H2Error::EnhanceYourCalm);
2703 }
2704 self.stream_table
2705 .set_expect_read(Some((H2StreamId::Zero, payload_len as usize)));
2706 let mut headers = headers.clone();
2707 headers.end_headers = flags & parser::FLAG_END_HEADERS != 0;
2708 // `header_block_fragment`'s window is no longer extended
2709 // here: it described an offset into `zero.storage`, which
2710 // this CONTINUATION frame's payload has not been read into
2711 // yet. `(H2State::ContinuationFrame(headers), _)` folds this
2712 // frame's payload into `self.header_reassembly` once it has
2713 // actually been read, and `handle_headers_frame` reads the
2714 // accumulator directly rather than through this field.
2715 self.state = H2State::ContinuationFrame(headers);
2716 }
2717 Err(error) => {
2718 let error = error_nom_to_h2(error);
2719 error!("{} COULD NOT PARSE CONTINUATION HEADER", log_context!(self));
2720 return self.goaway(error);
2721 }
2722 other => {
2723 error!(
2724 "{} UNEXPECTED {:?} WHILE PARSING CONTINUATION HEADER",
2725 log_context!(self),
2726 other
2727 );
2728 return self.goaway(H2Error::ProtocolError);
2729 }
2730 };
2731 MuxResult::Continue
2732 }
2733
2734 /// Ask the core what it wants read from the socket, so the caller can
2735 /// perform that read and report it back through [`Self::handle_read`].
2736 ///
2737 /// This is the read half of the same **two-call protocol** the write half
2738 /// already uses: `h2_transmit::gather` borrows the bytes to send, the
2739 /// caller writes them, `h2_transmit::confirm` is told how many landed.
2740 /// Here the core names the buffer it wants filled, the caller
2741 /// performs the read, and [`Self::handle_read`] is told how many bytes
2742 /// arrived and with what status.
2743 ///
2744 /// **This is not `AsyncRead::poll_read`.** Nothing here is a future,
2745 /// `context` is this module's [`Context`] and not a `task::Context`, and
2746 /// `lib/` holds no asynchronous function. The name follows
2747 /// `UdpManager::poll_output` (`protocol/udp/manager.rs`), this
2748 /// repository's existing spelling for "ask the core what it has for its
2749 /// caller", the way [`Self::handle_read`] follows that module's
2750 /// `UdpManager::handle_input`.
2751 ///
2752 /// Everything up to and including the decision of *which* buffer needs
2753 /// *how many* bytes lives on this side of the split, because two of the
2754 /// three answers are produced by that prelude: a SETTINGS-ACK timeout and
2755 /// an idle `expect_read` both end the pass with no read at all. A poll
2756 /// that could not say "nothing" would not be the core's answer.
2757 pub fn poll_read_target<E, L>(
2758 &mut self,
2759 context: &mut Context<L>,
2760 endpoint: &mut E,
2761 ) -> H2ReadTarget
2762 where
2763 E: Endpoint,
2764 L: ListenerHandler + L7ListenerHandler,
2765 {
2766 // Entry point: adopt the mux's snapshot for this pass.
2767 self.now = context.now;
2768 self.prune_inactive_streams_while_closing(context);
2769 // Pass 4 Medium #3: per-stream idle guard. Slow-multiplex Slowloris
2770 // sends one byte or a control frame per stream just often enough to
2771 // reset the connection-level timer; per-stream deadlines catch it.
2772 self.cancel_timed_out_streams(context, endpoint);
2773
2774 // RFC 9113 §6.5: check if peer has timed out on SETTINGS ACK
2775 if let Some(sent_at) = self.settings_sent_at
2776 && self.now.saturating_duration_since(sent_at) >= SETTINGS_ACK_TIMEOUT
2777 {
2778 warn!(
2779 "{} SETTINGS ACK timeout: no SETTINGS ACK observed within {:?}",
2780 log_context!(self),
2781 SETTINGS_ACK_TIMEOUT
2782 );
2783 return H2ReadTarget::Done(self.goaway(H2Error::SettingsTimeout));
2784 }
2785
2786 // Don't reset the timeout unconditionally here. Only application data
2787 // (DATA/HEADERS frames) should reset the timeout. H2 control frames
2788 // (PING, WINDOW_UPDATE, SETTINGS) must NOT reset it, otherwise a peer
2789 // sending periodic PINGs prevents timeout detection on stuck sessions.
2790 // The timeout is reset:
2791 // - Below, when reading DATA payload (H2StreamId::Other)
2792 // - In handle_frame(), when processing HEADERS frames
2793 // - On the write side, in handle_write(), once a stream transmit has
2794 // moved bytes — outbound application data is activity too, and an
2795 // acknowledgement-only write pass moves none of it
2796 let Some((stream_id, amount)) = self.stream_table.expect_read() else {
2797 self.readiness.event.remove(Ready::READABLE);
2798 return H2ReadTarget::Done(MuxResult::Continue);
2799 };
2800 // Every frame read can queue an answer in `self.output` (an ACK, a
2801 // RST_STREAM, a WINDOW_UPDATE). Past one buffer of queued output the
2802 // peer is not reading, so stop reading it too until that drains, as
2803 // HAProxy's demux stops on a full `mbuf` (`H2_CF_DEM_MROOM`). The event
2804 // is kept for the later read, and `Self::consume_output` restores the
2805 // interest. A forced READABLE (the shutdown drive) is stopped as well.
2806 if self.output.len() >= self.output_read_cap() {
2807 self.reads_wait_for_output = true;
2808 self.readiness.interest.remove(Ready::READABLE);
2809 return H2ReadTarget::Done(MuxResult::Continue);
2810 }
2811 match stream_id {
2812 H2StreamId::Zero => {}
2813 H2StreamId::Other { .. } => {
2814 // Reading DATA frame payload for an application stream.
2815 // This is real application activity — reset the timeout.
2816 self.arm_timeout();
2817 }
2818 }
2819 let skipping = self.skipping_orphaned_payload();
2820 let kawa = read_buffer(
2821 &mut self.zero,
2822 &mut context.streams,
2823 &self.position,
2824 stream_id,
2825 );
2826 trace!(
2827 "{} {:?}({:?}, {})",
2828 log_context!(self),
2829 self.state,
2830 stream_id,
2831 amount
2832 );
2833 if amount > 0 {
2834 let available = kawa.storage.available_space();
2835 // An orphaned DATA remainder is read and dropped, never kept, so
2836 // it only needs *some* room: read what fits and come back for the
2837 // rest instead of waiting for room for all of it.
2838 let amount = if skipping {
2839 amount.min(available)
2840 } else {
2841 amount
2842 };
2843 if amount == 0 || amount > available {
2844 self.readiness.interest.remove(Ready::READABLE);
2845 return H2ReadTarget::Done(MuxResult::Continue);
2846 }
2847 H2ReadTarget::Fill { stream_id, amount }
2848 } else {
2849 self.stream_table.set_expect_read(None);
2850 H2ReadTarget::Skip(stream_id)
2851 }
2852 }
2853
2854 /// Tell the core what the caller's read actually did, then let it consume
2855 /// the bytes and dispatch the frame they completed.
2856 ///
2857 /// The second half of [`Self::poll_read_target`]'s protocol. `stream_id`
2858 /// is the one the matching [`H2ReadTarget`] named, and `outcome` answers
2859 /// that same variant: [`H2ReadOutcome::Skipped`] for
2860 /// [`H2ReadTarget::Skip`], [`H2ReadOutcome::Filled`] for
2861 /// [`H2ReadTarget::Fill`].
2862 ///
2863 /// `read_buffer` is called a second time here rather than carried across
2864 /// the split: `StreamParts` borrows `context`, so a buffer held across the
2865 /// caller's read would pin `context.debug` and `Self::handle_frame`'s whole
2866 /// `&mut Context` with it. Re-deriving it from a `Copy` [`H2StreamId`] is
2867 /// a match and a field projection.
2868 pub fn handle_read<E, L>(
2869 &mut self,
2870 context: &mut Context<L>,
2871 endpoint: E,
2872 stream_id: H2StreamId,
2873 outcome: H2ReadOutcome,
2874 ) -> MuxResult
2875 where
2876 E: Endpoint,
2877 L: ListenerHandler + L7ListenerHandler,
2878 {
2879 let skipping = self.skipping_orphaned_payload();
2880 let kawa = read_buffer(
2881 &mut self.zero,
2882 &mut context.streams,
2883 &self.position,
2884 stream_id,
2885 );
2886 match outcome {
2887 H2ReadOutcome::Skipped => {}
2888 H2ReadOutcome::Filled {
2889 amount,
2890 size,
2891 status,
2892 } => {
2893 let did = match stream_id {
2894 H2StreamId::Zero => usize::MAX,
2895 H2StreamId::Other {
2896 gid: global_stream_id,
2897 ..
2898 } => global_stream_id,
2899 };
2900 context.debug.push(DebugEvent::SocketIO(0, did, size));
2901 if !skipping {
2902 kawa.storage.fill(size);
2903 }
2904 let event = self.position.bytes_in_event(size);
2905 self.metric_events.push(event);
2906 self.bytes.zero_bytes_read += size;
2907 if update_readiness_after_read(size, status, &mut self.readiness) {
2908 if matches!(self.position, Position::Server)
2909 && self.drain.draining()
2910 && matches!(status, SocketResult::Closed | SocketResult::Error)
2911 {
2912 // During graceful drain, a frontend EOF/HUP means no
2913 // further frame headers or payload bytes can arrive.
2914 // Keeping expect_read here strands the connection in
2915 // Header/Frame forever even after the peer is gone.
2916 self.stream_table.set_expect_read(None);
2917 }
2918 return MuxResult::Continue;
2919 } else if skipping {
2920 // `amount` was capped to the room in `zero`; the debt is
2921 // what `expect_read` still names.
2922 let remaining = self
2923 .stream_table
2924 .expect_read()
2925 .map_or(0, |(_, remaining)| remaining.saturating_sub(size));
2926 if remaining > 0 {
2927 self.stream_table
2928 .set_expect_read(Some((stream_id, remaining)));
2929 return MuxResult::Continue;
2930 }
2931 self.stream_table.set_expect_read(None);
2932 } else if size == amount {
2933 self.stream_table.set_expect_read(None);
2934 } else {
2935 self.stream_table
2936 .set_expect_read(Some((stream_id, amount - size)));
2937 if let (H2State::ClientPreface, Position::Server) =
2938 (&self.state, &self.position)
2939 {
2940 let (pri, i) = (serializer::H2_PRI.as_bytes(), kawa.storage.data());
2941 if !pri.starts_with(&i[..i.len().min(pri.len())]) {
2942 debug!("{} EARLY INVALID PREFACE: {:?}", log_context!(self), i);
2943 return self.force_disconnect();
2944 }
2945 }
2946 return MuxResult::Continue;
2947 }
2948 }
2949 }
2950 match (&self.state, &self.position) {
2951 (H2State::Error, _)
2952 | (H2State::GoAway, _)
2953 | (H2State::ServerSettings, Position::Server)
2954 | (H2State::ClientPreface, Position::Client(..))
2955 | (H2State::ClientSettings, Position::Client(..)) => {
2956 error!(
2957 "{} Unexpected combination: (Readable, {:?}, {:?})",
2958 log_context!(self),
2959 self.state,
2960 self.position
2961 );
2962 return self.force_disconnect();
2963 }
2964 (H2State::Discard, _) => {
2965 let i = kawa.storage.data();
2966 trace!("{} DISCARDING: {:?}", log_context!(self), i);
2967 // RFC 9113 §4.3: HPACK field-compression state is scoped to
2968 // the connection, not the stream. `i` is this refused
2969 // frame's own just-read payload; decode it (folding in any
2970 // earlier frames' bytes for a CONTINUATION refusal — see
2971 // `DiscardedFieldBlock`) before it is dropped, so our
2972 // decoder does not fall behind the peer's encoder.
2973 //
2974 // Without a field block this is an orphaned DATA remainder
2975 // (`Self::skip_orphaned_data_payload`): its bytes were never
2976 // kept, and `zero` may still hold unflushed output.
2977 if let Some((stream_id, discarded)) = self.discarded_field_block.take() {
2978 match decode_discarded_field_block(self.hpack.decoder_mut(), i, discarded) {
2979 // The block continues on CONTINUATION frames.
2980 Ok(Some(fragment)) => {
2981 self.pending_discarded_block = Some((stream_id, fragment));
2982 }
2983 Ok(None) => self.flood_detector.reset_continuation(),
2984 Err(error) => {
2985 error!(
2986 "{} discarded stream's HPACK field block failed to decode: {:?}",
2987 log_context!(self),
2988 error
2989 );
2990 return self.goaway(error);
2991 }
2992 }
2993 kawa.storage.clear();
2994 }
2995 self.attribute_bytes_to_overhead();
2996 self.expect_header();
2997 }
2998 (H2State::ClientPreface, Position::Server) => {
2999 let i = kawa.storage.data();
3000 let i = match parser::preface(i) {
3001 Ok((i, _)) => i,
3002 Err(_) => return self.force_disconnect(),
3003 };
3004 match parser::frame_header(i, self.local_settings.settings_max_frame_size) {
3005 Ok((
3006 _,
3007 FrameHeader {
3008 payload_len,
3009 frame_type: FrameType::Settings,
3010 flags: 0,
3011 stream_id: 0,
3012 },
3013 )) => {
3014 kawa.storage.clear();
3015 self.state = H2State::ClientSettings;
3016 self.stream_table
3017 .set_expect_read(Some((H2StreamId::Zero, payload_len as usize)));
3018 }
3019 _ => return self.force_disconnect(),
3020 };
3021 }
3022 (H2State::ClientSettings, Position::Server) => {
3023 let i = kawa.storage.data();
3024 let settings = match parser::settings_frame(
3025 i,
3026 &FrameHeader {
3027 payload_len: i.len() as u32,
3028 frame_type: FrameType::Settings,
3029 flags: 0,
3030 stream_id: 0,
3031 },
3032 ) {
3033 Ok((_, settings)) => {
3034 kawa.storage.clear();
3035 settings
3036 }
3037 Err(_) => return self.force_disconnect(),
3038 };
3039 let local_settings = &self.local_settings;
3040 match self.output.push_frames(SETTINGS_FRAME_SIZE, |buf| {
3041 serializer::gen_settings(buf, local_settings).map(|(_, size)| size)
3042 }) {
3043 Ok(_) => {
3044 self.metric_events.push(MetricEvent::SettingsFrameSent);
3045 // RFC 9113 §6.5: start tracking SETTINGS ACK timeout
3046 self.settings_sent_at = Some(self.now);
3047 }
3048 Err(error) => {
3049 error!(
3050 "{} Could not serialize SettingsFrame: {:?}",
3051 log_context!(self),
3052 error
3053 );
3054 return self.force_disconnect();
3055 }
3056 };
3057 // Enlarge the connection-level receive window beyond the RFC
3058 // default of 65 535 bytes, in the same output flush as the
3059 // SETTINGS above and the ACK `handle_frame` appends below: one
3060 // `writev(2)` for the whole server preface instead of a second
3061 // write pass for a queued WINDOW_UPDATE. RFC 9113 §3.4 only
3062 // requires SETTINGS to come first. Skipped when the configured
3063 // window is the default, since a zero-increment WINDOW_UPDATE
3064 // violates RFC 9113 §6.9. Our own send window is untouched:
3065 // this enlarges the peer's send allowance, not ours.
3066 let increment = self
3067 .connection_config
3068 .initial_connection_window
3069 .saturating_sub(DEFAULT_INITIAL_WINDOW_SIZE);
3070 self.connection_window_enlarged = true;
3071 if increment > 0 {
3072 match self.output.push_frames(WINDOW_UPDATE_FRAME_SIZE, |buf| {
3073 serializer::gen_window_update(buf, 0, increment).map(|(_, size)| size)
3074 }) {
3075 Ok(_) => {
3076 self.metric_events
3077 .push(MetricEvent::WindowUpdateFramesSent(1));
3078 }
3079 Err(error) => {
3080 error!(
3081 "{} Could not serialize the connection WINDOW_UPDATE: {:?}",
3082 log_context!(self),
3083 error
3084 );
3085 return self.force_disconnect();
3086 }
3087 }
3088 }
3089
3090 self.state = H2State::ServerSettings;
3091 self.readiness.signal_pending_write();
3092 return self.handle_frame(settings, 0, context, endpoint);
3093 }
3094 (H2State::ServerSettings, Position::Client(..)) => {
3095 let i = kawa.storage.data();
3096 match parser::frame_header(i, self.local_settings.settings_max_frame_size) {
3097 Ok((
3098 _,
3099 header @ FrameHeader {
3100 payload_len,
3101 frame_type: FrameType::Settings,
3102 flags: 0,
3103 stream_id: 0,
3104 },
3105 )) => {
3106 kawa.storage.clear();
3107 self.stream_table
3108 .set_expect_read(Some((H2StreamId::Zero, payload_len as usize)));
3109 self.state = H2State::Frame(header)
3110 }
3111 _ => return self.force_disconnect(),
3112 };
3113 }
3114 (H2State::Header, _) => {
3115 return self.handle_header_state(context, endpoint);
3116 }
3117 (H2State::ContinuationHeader(headers), _) => {
3118 let headers = headers.clone();
3119 return self.handle_continuation_header_state(&headers);
3120 }
3121 (H2State::Frame(header), _) => {
3122 let i = kawa.storage.unparsed_data();
3123 trace!("{} data: {:?}", log_context!(self), i);
3124 let wire_payload_len = header.payload_len;
3125 let frame = match parser::frame_body(i, header) {
3126 Ok((_, frame)) => frame,
3127 Err(error) => {
3128 let error = error_nom_to_h2(error);
3129 // RFC 9113 §6.3: a PRIORITY frame of the wrong length
3130 // is a stream error, unlike every other frame size
3131 // error (§4.2). PRIORITY is read through stream 0.
3132 if header.frame_type == FrameType::Priority
3133 && error == H2Error::FrameSizeError
3134 {
3135 let priority_stream_id = header.stream_id;
3136 kawa.storage.end = kawa.storage.head;
3137 self.expect_header();
3138 return self.handle_priority_frame_size_error(
3139 priority_stream_id,
3140 context,
3141 endpoint,
3142 );
3143 }
3144 error!("{} COULD NOT PARSE FRAME BODY", log_context!(self));
3145 return self.goaway(error);
3146 }
3147 };
3148 if let H2StreamId::Zero = stream_id {
3149 // Free `zero.storage` for the next frame header read. A
3150 // HEADERS frame used to be special-cased here
3151 // (`head = end`, preserving its payload bytes in place
3152 // so a following CONTINUATION could extend them
3153 // in-buffer) — the reassembly accumulator that needed
3154 // now lives in `self.header_reassembly` instead (see
3155 // `handle_headers_frame`), copied out before this read
3156 // cycle ends, so every zero-stream frame gets the same
3157 // treatment.
3158 kawa.storage.end = kawa.storage.head;
3159 }
3160 self.expect_header();
3161 return self.handle_frame(frame, wire_payload_len, context, endpoint);
3162 }
3163 (H2State::ContinuationFrame(headers), _) => {
3164 // This CONTINUATION frame's payload has just finished being
3165 // read into `zero.storage` (the generic stream-0 read
3166 // dispatch above, shared by every zero-stream frame type).
3167 // Move it into the owned reassembly accumulator and free
3168 // `zero.storage` immediately — from this point until the
3169 // next CONTINUATION's bytes start arriving, `zero.storage`
3170 // holds nothing belonging to this header block. See
3171 // `h2_header_reassembly.rs` and LIFECYCLE.md invariant 24.
3172 self.header_reassembly.append(kawa.storage.data());
3173 kawa.storage.clear();
3174 let headers = headers.clone();
3175 self.expect_header();
3176 return self.handle_frame(Frame::Headers(headers), 0, context, endpoint);
3177 }
3178 }
3179 MuxResult::Continue
3180 }
3181
3182 /// Update the H2 connection-level *aggregate* gauges with this connection's
3183 /// current contribution, expressed as a signed delta against the last
3184 /// snapshot we emitted.
3185 ///
3186 /// The four metrics are returned as [`MetricEvent`] signed deltas (rather
3187 /// than absolute values) so that the dashboard sees the **sum across all
3188 /// live H2 connections**:
3189 ///
3190 /// - `h2.connection.window_bytes` — sum of available connection-level
3191 /// send-window bytes. Negative per-connection windows clamp to 0 so the
3192 /// aggregate represents only available capacity, not deficit.
3193 /// - `h2.connection.active_streams` — sum of in-flight streams across
3194 /// every H2 connection.
3195 /// - `h2.connection.pending_window_updates` — sum of queued (un-flushed)
3196 /// per-stream WINDOW_UPDATE entries across every H2 connection.
3197 /// - `h2.streams.ready_incremental.by_urgency` — sum of
3198 /// [`Self::ready_incremental_streams`] across every H2 connection.
3199 ///
3200 /// Called from the write hot path; emits nothing when the snapshot is
3201 /// unchanged so the steady state stays cheap. The paired decrement for
3202 /// every increment is provided by [`Self::release_connection_gauges`],
3203 /// which subtracts the final snapshot when the connection is dropped —
3204 /// keeping the aggregate arithmetically symmetric independent of which
3205 /// close path runs (`graceful_goaway`, `force_disconnect`,
3206 /// `handle_goaway_frame`, `Mux::close`, panic-unwind, …).
3207 /// [`H2Shell`]'s [`Drop`] is the single site that calls it.
3208 fn gauge_connection_state(&mut self) {
3209 let snapshot = (
3210 self.flow_control.window().max(0) as usize,
3211 self.stream_table.streams().len(),
3212 self.flow_control.pending_window_updates_len(),
3213 self.ready_incremental_streams,
3214 );
3215 if self.last_gauge_snapshot == Some(snapshot) {
3216 return;
3217 }
3218 let prev = self.last_gauge_snapshot.unwrap_or((0, 0, 0, 0));
3219 // Diff in i64 — usize cannot represent the negative side of the delta.
3220 let dw = snapshot.0 as i64 - prev.0 as i64;
3221 let ds = snapshot.1 as i64 - prev.1 as i64;
3222 let du = snapshot.2 as i64 - prev.2 as i64;
3223 let dr = snapshot.3 as i64 - prev.3 as i64;
3224 if dw != 0 {
3225 self.metric_events
3226 .push(MetricEvent::ConnectionWindowBytes(dw));
3227 }
3228 if ds != 0 {
3229 self.metric_events
3230 .push(MetricEvent::ConnectionActiveStreams(ds));
3231 }
3232 if du != 0 {
3233 self.metric_events
3234 .push(MetricEvent::ConnectionPendingWindowUpdates(du));
3235 }
3236 if dr != 0 {
3237 self.metric_events
3238 .push(MetricEvent::StreamsReadyIncrementalByUrgency(dr));
3239 }
3240 self.last_gauge_snapshot = Some(snapshot);
3241 }
3242
3243 /// Subtract this connection's contribution from the four aggregate gauges
3244 /// [`Self::gauge_connection_state`] feeds. Idempotent: clears
3245 /// `last_gauge_snapshot` so a second call (or a `Drop` on top of an
3246 /// explicit reset) is a no-op.
3247 ///
3248 /// Pairs with every prior call to [`Self::gauge_connection_state`]. Queues
3249 /// the negatives onto [`Self::metric_events`] like every other core-side
3250 /// metric; the caller is [`H2Shell`]'s [`Drop`], which calls this and then
3251 /// drains, so the symmetry is guaranteed regardless of the close path.
3252 ///
3253 /// Stays module-private: the release has to run exactly once per
3254 /// connection, at teardown, and that single caller is in this module
3255 /// alongside it.
3256 fn release_connection_gauges(&mut self) {
3257 if let Some((w, s, u, r)) = self.last_gauge_snapshot.take() {
3258 if w != 0 {
3259 self.metric_events
3260 .push(MetricEvent::ConnectionWindowBytes(-(w as i64)));
3261 }
3262 if s != 0 {
3263 self.metric_events
3264 .push(MetricEvent::ConnectionActiveStreams(-(s as i64)));
3265 }
3266 if u != 0 {
3267 self.metric_events
3268 .push(MetricEvent::ConnectionPendingWindowUpdates(-(u as i64)));
3269 }
3270 if r != 0 {
3271 self.metric_events
3272 .push(MetricEvent::StreamsReadyIncrementalByUrgency(-(r as i64)));
3273 }
3274 }
3275 }
3276
3277 /// Take every [`MetricEvent`] this core has queued since the last drain.
3278 ///
3279 /// The core's half of the boundary: it states what happened and by how
3280 /// much, and stops there. Recording is [`record_metric`]'s job, and
3281 /// [`H2Shell::record_pending_metrics`] is the production caller. Reading
3282 /// the queue instead is what will let a driver with no `METRICS` registry
3283 /// — `sim/tests/h2_simulation.rs`, once the slice needing it exports the
3284 /// type — fold the events into a shadow model, as `udp_simulation.rs` does.
3285 pub(super) fn drain_metric_events(&mut self) -> std::vec::Drain<'_, MetricEvent> {
3286 self.metric_events.drain(..)
3287 }
3288
3289 /// Ask the core what it wants written to the socket, so the caller can
3290 /// perform that write and report it back through [`Self::handle_write`].
3291 ///
3292 /// The write half of [`Self::poll_read_target`]'s protocol, with ONE forced
3293 /// divergence: a read pass ENDS with `handle_read`, while a write pass
3294 /// performs an unbounded number of writes, so this is re-entered until it
3295 /// answers [`H2WriteTarget::Done`] or [`H2WriteTarget::Finalize`]. The
3296 /// `pass` parameter is what makes that re-entry meaningful: 9.6a needed no
3297 /// stash because `expect_read` already lived on `stream_table`, whereas one
3298 /// write pass builds a dozen values that have to survive the round trip.
3299 ///
3300 /// It is a `&mut` local of `Self::write_streams` and not an
3301 /// `Option<H2WritePass>` field on `self`: every exit ends the pass, so it
3302 /// never outlives one call, and resumption ACROSS calls is already
3303 /// `H2StreamTable::expect_write`.
3304 ///
3305 /// **A caller that stops driving after an [`H2WriteTarget::Transmit`]
3306 /// strands the pass's converter**, and with it the three HPACK scratch
3307 /// buffers it took out of `hpack_state::HpackState` — silently, and for
3308 /// the life of the connection. Between the transition into
3309 /// `H2WritePhase::Prepare` and the first statement of
3310 /// `H2WritePhase::End` this function has exactly one `return`, the
3311 /// `Transmit` yield, so the drive loop is the whole guarantee;
3312 /// `H2WritePass`'s [`Drop`] carries the matching `debug_assert!`. A
3313 /// re-entry AFTER the pass answered is the other half of that argument and
3314 /// is handled separately: `End` moves the pass to
3315 /// `H2WritePhase::Ended`, whose arm answers `Done` rather than
3316 /// re-entering an arm whose first statement takes three `Option`s it has
3317 /// already emptied.
3318 ///
3319 /// Unlike [`Self::handle_write`] this never sees a [`SocketResult`]. The
3320 /// pass terminator is `pass.stalled`, written in exactly one statement over
3321 /// there, and `size > 0` with `SocketResult::WouldBlock` is NOT a stall —
3322 /// `update_readiness` decides on the size alone. A terminator keyed on the
3323 /// status would truncate every response a TLS frontend accepts in two
3324 /// rounds, which is why adding that parameter here has to be a visible
3325 /// signature change rather than a one-token edit.
3326 pub fn poll_write_target<E, L>(
3327 &mut self,
3328 context: &mut Context<L>,
3329 endpoint: &mut E,
3330 pass: &mut H2WritePass,
3331 ) -> H2WriteTarget
3332 where
3333 E: Endpoint,
3334 L: ListenerHandler + L7ListenerHandler,
3335 {
3336 loop {
3337 match pass.phase {
3338 H2WritePhase::Start => {
3339 // sozu-proxy/sozu#1641: DATA the last pass left parked
3340 // and that is no longer queued was dropped unsent. Its
3341 // stream is still here (a removal refunds eagerly and
3342 // clears the park), so both windows get it back; the
3343 // DATA still queued goes out in the resume below.
3344 let parked_data = std::mem::take(&mut self.parked_data);
3345 if parked_data > 0 {
3346 match self.stream_table.expect_write() {
3347 Some(H2StreamId::Other { gid, .. }) => {
3348 let parts = context.streams[gid].split(&self.position);
3349 let queued = parked_data_credit(parts.wbuffer);
3350 debug_assert!(
3351 queued <= parked_data,
3352 "nothing adds DATA to a parked stream before it resumes"
3353 );
3354 let dropped = parked_data - queued;
3355 if dropped > 0 {
3356 *parts.window = parts.window.saturating_add(dropped);
3357 if let Some(result) = self.refund_parked_data(dropped) {
3358 return H2WriteTarget::Done(result);
3359 }
3360 }
3361 }
3362 // The park was cleared without a removal: the
3363 // frontend hung up while draining.
3364 _ => {
3365 if let Some(result) = self.refund_parked_data(parked_data) {
3366 return H2WriteTarget::Done(result);
3367 }
3368 }
3369 }
3370 }
3371 debug_assert_eq!(self.parked_data, 0, "the park's DATA is settled here");
3372 // sozu-proxy/sozu#1627: a header block the last pass left
3373 // parked and unsent is gone if its stream was removed or
3374 // its `kawa.out` cleared. Nothing encoded after it is
3375 // queued anywhere, so emptying the encoder's table here,
3376 // before this pass encodes a block, resynchronises the
3377 // peer through that block's leading size updates.
3378 if std::mem::take(&mut self.parked_header_block) {
3379 let dropped = match self.stream_table.expect_write() {
3380 Some(parked @ H2StreamId::Other { .. }) => write_buffer(
3381 &mut self.zero,
3382 &mut context.streams,
3383 &self.position,
3384 parked,
3385 )
3386 .out
3387 .is_empty(),
3388 _ => true,
3389 };
3390 if dropped {
3391 self.reset_encoder_table();
3392 }
3393 }
3394 if let Some(
3395 write_stream @ H2StreamId::Other {
3396 id: stream_id,
3397 gid: global_stream_id,
3398 },
3399 ) = self.stream_table.expect_write()
3400 {
3401 // Resume path: if the same stream is parked waiting for
3402 // buffer space (expect_read matches write_stream), keep
3403 // the amount so `handle_write` can re-enable READABLE as
3404 // soon as we drain.
3405 let cross_read_amount = match self.stream_table.expect_read() {
3406 Some((read_stream, amount)) if write_stream == read_stream => {
3407 Some(amount)
3408 }
3409 _ => None,
3410 };
3411 pass.phase = H2WritePhase::Resume {
3412 stream_id,
3413 gid: global_stream_id,
3414 cross_read_amount,
3415 // Read BEFORE the flush, where the pre-image read
3416 // it, and carried across the transmits rather than
3417 // re-read after them: its consumer is
3418 // `handle_1xx_reset`, so a stream retired mid-flush
3419 // must not change which state that check sees.
3420 stream_state: context.streams[global_stream_id].state,
3421 };
3422 } else {
3423 self.begin_scheduler_pass(context, pass);
3424 }
3425 }
3426 H2WritePhase::Resume {
3427 stream_id,
3428 gid: global_stream_id,
3429 stream_state,
3430 ..
3431 } => {
3432 let write_stream = H2StreamId::Other {
3433 id: stream_id,
3434 gid: global_stream_id,
3435 };
3436 // The pre-image's `while !kawa.out.is_empty()`, as a
3437 // resumption state. `pass.stalled` is the loop's ONLY
3438 // terminator, and it is false on the first entry.
3439 if !pass.stalled {
3440 let kawa = write_buffer(
3441 &mut self.zero,
3442 &mut context.streams,
3443 &self.position,
3444 write_stream,
3445 );
3446 // The queue can hold the adopted rest of this very
3447 // stream's frame: it goes out first, in this transmit.
3448 if !kawa.out.is_empty() || !self.output.is_empty() {
3449 return H2WriteTarget::Transmit {
3450 stream_id: write_stream,
3451 };
3452 }
3453 }
3454 let stream = &mut context.streams[global_stream_id];
3455 let parts = stream.split(&self.position);
3456 let kawa = parts.wbuffer;
3457 // Refresh the per-stream idle timer when outbound bytes move: a
3458 // large response delivered at low bandwidth is "active", not idle,
3459 // even when the peer sends no inbound frames.
3460 if pass.resume_bytes > 0 {
3461 self.stream_table.touch_activity(stream_id, self.now);
3462 // Clear the flow-control-stall deadline ONLY when the effective
3463 // send window is genuinely open — that alone is a real un-stall.
3464 // A window-stalled stream can flush a `WINDOW_UPDATE(+1)`-drip
3465 // byte HERE via socket-backpressure resume; clearing on that
3466 // would reset the deadline at 1-byte granularity and re-open the
3467 // drip the M2 cumulative-stall budget closes. While still blocked,
3468 // leave the deadline (and its progress accumulator) for the main
3469 // write loop's budget to govern — keeping the two maps in lockstep.
3470 if min(*parts.window, self.flow_control.window()) > 0 {
3471 self.stream_table.clear_fc_stall(stream_id);
3472 }
3473 }
3474 if pass.stalled {
3475 // The FIRST of the two terminators that must not
3476 // finalize: the scheduler pass never began, so LIFECYCLE
3477 // §9 invariant 16's readiness policy has nothing to
3478 // decide and the park must survive untouched.
3479 self.parked_header_block = h2_transmit::holds_header_frame(kawa);
3480 self.parked_data = parked_data_credit(kawa);
3481 return H2WriteTarget::Done(MuxResult::Continue);
3482 }
3483 self.stream_table.set_expect_write(None);
3484 if (kawa.is_terminated() || kawa.is_error())
3485 && kawa.is_completed()
3486 && !Self::handle_1xx_reset(kawa, stream_state, endpoint)
3487 {
3488 let (client_rtt, server_rtt) =
3489 self.snapshot_rtts(endpoint, stream.linked_token());
3490
3491 if let Some((dead_id, token)) = self.try_recycle_server_stream(
3492 stream,
3493 global_stream_id,
3494 stream_id,
3495 pass.byte_totals,
3496 &mut context.debug,
3497 context.listener.clone(),
3498 client_rtt,
3499 server_rtt,
3500 ) {
3501 // Remove the recycled stream from the connection maps
3502 // before endpoint.end_stream() can trigger teardown.
3503 // Otherwise session close can observe a stale `Recycle`
3504 // entry in self.streams and mis-handle the connection as
3505 // if it still had an active H2 stream.
3506 self.remove_dead_stream(dead_id, global_stream_id);
3507 if let Some(token) = token {
3508 remove_backend_stream(
3509 &mut context.backend_streams,
3510 token,
3511 global_stream_id,
3512 );
3513 endpoint.end_stream(token, global_stream_id, context);
3514 }
3515 }
3516 }
3517 self.begin_scheduler_pass(context, pass);
3518 }
3519 H2WritePhase::Prepare { cursor } => {
3520 let Some(&stream_id) = pass.order().get(cursor) else {
3521 pass.phase = H2WritePhase::End;
3522 continue;
3523 };
3524 let Some(&global_stream_id) = self.stream_table.streams().get(&stream_id)
3525 else {
3526 error!(
3527 "{} stream_id {} from sorted keys missing in streams map",
3528 log_context!(self),
3529 stream_id
3530 );
3531 pass.phase = H2WritePhase::Prepare { cursor: cursor + 1 };
3532 continue;
3533 };
3534 let (urgency, is_incremental) = self.scheduler.priority(&stream_id);
3535 let stream = &mut context.streams[global_stream_id];
3536 // Read BEFORE the flush, where the pre-image read it, and
3537 // carried through `H2WritePhase::Flush` — see that field's
3538 // doc.
3539 let stream_state = stream.state;
3540 let parts = stream.split(&self.position);
3541 let kawa = parts.wbuffer;
3542 pass.consumed = 0;
3543 // The pre-image re-ASSIGNED `stalled` from each
3544 // `flush_stream_out` call, and a stream with an empty queue
3545 // flushed zero rounds and read Drained. Clearing it here is
3546 // what keeps one stream's stall from terminating the next
3547 // stream's flush before its first transmit.
3548 pass.stalled = false;
3549 // A request in error on a backend stream whose HEADERS
3550 // never left (the H1 frontend rejected the body and
3551 // answered 400 itself, say) must not reach the converter,
3552 // whose `initialize` would queue a RST_STREAM for a stream
3553 // the backend has never seen: a connection error on the
3554 // shared connection (RFC 9113 §5.1, §6.4,
3555 // sozu-proxy/sozu#1631). There is nothing to cancel; the
3556 // stream is left for `end_stream` or the idle reaper, which
3557 // retire it without a frame.
3558 let never_opened_error = kawa.is_error()
3559 && !kawa.consumed
3560 && matches!(self.position, Position::Client(..));
3561 #[cfg(debug_assertions)]
3562 let out_before = kawa.out.len();
3563 if kawa.is_main_phase()
3564 || (kawa.is_terminated() && !kawa.is_completed())
3565 || (kawa.is_error()
3566 && !never_opened_error
3567 && !self.stream_table.rst_sent_contains(stream_id))
3568 {
3569 let window = min(*parts.window, self.flow_control.window());
3570 // Same-urgency-bucket ready-peer count (Tier 3a, LIFECYCLE §9
3571 // invariant 17). The converter skips the yield when there is
3572 // no peer in the same bucket to interleave with — prevents
3573 // the `finalize_write` WRITABLE-withdrawal strand (see
3574 // `test_h2_solo_incremental_drains_fully`). A connection-wide
3575 // count would wrongly yield for a solo incremental stream
3576 // when another urgency bucket happens to contain an
3577 // incremental peer.
3578 let incremental_peer_count =
3579 pass.census_mut().incremental_peer_count(urgency);
3580 // Track RST_STREAM dedup: if kawa is in error state, the converter
3581 // will generate a RST_STREAM frame via `initialize`. Mark it so we
3582 // don't send a duplicate on the next writable cycle.
3583 if kawa.is_error() {
3584 let freshly_rst = self.stream_table.rst_sent_mut().insert(stream_id);
3585 // LIFECYCLE §9 invariant 17: any transition to ineligible
3586 // mid-pass MUST leave the pass census so later streams in
3587 // the same iteration see the live count, not the
3588 // snapshot. Missing this costs one voluntary yield per
3589 // same-urgency peer that trails the RST.
3590 if freshly_rst {
3591 pass.census_mut().note_ineligible(urgency, is_incremental);
3592 }
3593 // Account for the RST that `initialize` is about to emit
3594 // for this stream, so the per-error breakdown and the
3595 // global tx counter see every RST that reaches the wire:
3596 // any path that flips `parsing_phase` to Error before
3597 // reaching this gate (oversized trailers, malformed
3598 // bodies, etc.) would otherwise land an unaccounted RST.
3599 // The accounting call is deferred to after the loop; see
3600 // `H2WritePass::freshly_emitted_rsts`.
3601 if freshly_rst {
3602 pass.freshly_emitted_rsts.push(rst_error_from_kawa(kawa));
3603 }
3604 }
3605 // Apply per-frontend response-side header edits
3606 // (set/replace/delete) stashed by the routing layer at
3607 // request time. H2 frontends always run as Server
3608 // position; the back-side H2 client (when sozu speaks
3609 // H2 to a backend) is a request emission and was
3610 // already mutated by Router::route_from_request.
3611 //
3612 // The snapshot is **drained** via `mem::take` so the
3613 // injection runs exactly once per response. Without
3614 // this, a re-entry of `write_streams` for the same
3615 // stream (multi-frame body, flow-control yield, or
3616 // RFC 9218 same-urgency round-robin) would re-call
3617 // `apply_response_header_edits` after `kawa.prepare`
3618 // had already consumed the `Block::Flags{end_header}`
3619 // anchor — the helper falls back to
3620 // `kawa.blocks.len()` and appends the edit AFTER all
3621 // remaining DATA blocks. The next prepare cycle then
3622 // encodes that orphan `Block::Header` into
3623 // `H2BlockConverter.out` with no closing
3624 // `Block::Flags{end_header}` to flush it as a HEADERS
3625 // frame, and `H2BlockConverter::finalize` trips the
3626 // "out buffer not empty (38 bytes remaining), clearing"
3627 // defense-in-depth log on every re-entry. 38 bytes is
3628 // the static-table HPACK encoding of a typical HSTS
3629 // header, which is how the symptom surfaces in
3630 // production once the listener-default HSTS reaches a
3631 // non-trivial share of frontends.
3632 if matches!(self.position, super::Position::Server)
3633 && !parts.context.headers_response.is_empty()
3634 {
3635 let edits = std::mem::take(&mut parts.context.headers_response);
3636 super::shared::apply_response_header_edits(kawa, &edits);
3637 }
3638 // One converter for exactly this `prepare` call. It borrows
3639 // the encoder out of `self.hpack`; keeping that borrow no
3640 // longer than the call is what leaves the rest of this
3641 // phase free to take `&self` / `&mut self`. The scratch and
3642 // the RFC 7541 §6.3 signal are moved in here and moved back
3643 // out by `reclaim` below — no buffer is copied.
3644 //
3645 // RFC 9218 §4: `incremental_mode` makes an incremental
3646 // stream yield the converter after a single DATA frame so
3647 // same-urgency peers interleave; a non-scheduled caller
3648 // (the resume phase above, the converter's own unit tests)
3649 // keeps the sequential semantics with `false`.
3650 let mut converter = pass.converter_mut().converter(
3651 self.hpack.encoder_mut(),
3652 stream_id,
3653 window,
3654 is_incremental,
3655 incremental_peer_count,
3656 );
3657 kawa.prepare(&mut converter);
3658 // sozu-proxy/sozu#1632: what a backend connection
3659 // encoded belongs to it (its stream id, its HPACK
3660 // table) and cannot be sent on another one.
3661 if matches!(self.position, Position::Client(..)) && !kawa.out.is_empty() {
3662 *parts.front_bound_to_backend = true;
3663 }
3664 let events_before = self.metric_events.len();
3665 let remaining = pass
3666 .converter_mut()
3667 .reclaim(converter, &mut self.metric_events);
3668 // Each DATA frame the converter just wrote earns the
3669 // peer stream-0 WINDOW_UPDATE credit. `reclaim` appended
3670 // this prepare's events to the tail, one
3671 // `DataFrameSent` per DATA frame serialized.
3672 let data_frames_sent = self.metric_events[events_before..]
3673 .iter()
3674 .filter(|event| matches!(event, MetricEvent::DataFrameSent))
3675 .count() as u64;
3676 self.flood_detector
3677 .record_data_frames_sent(data_frames_sent);
3678 pass.consumed = window - remaining;
3679 // The pre-prepare gate above only inserts into
3680 // `rst_sent` when `kawa.is_error()` is already true on
3681 // entry. The HPACK over-budget abort path
3682 // (`H2BlockConverter::check_header_capacity` →
3683 // `finalize`) flips `parsing_phase` to Error AND pushes
3684 // its own RST_STREAM frame inside this same prepare
3685 // pass; without a post-prepare insert here the next
3686 // writable cycle would gate-pass and double-emit a
3687 // RST_STREAM via the existing `initialize` chokepoint.
3688 //
3689 // Per Codex P2: the converter's direct RST emission
3690 // bypasses the metric accounting that
3691 // `Self::reset_stream` performs. Mirror it here so
3692 // dashboards see the per-error counter and the global
3693 // tx counter.
3694 //
3695 // Per Codex P3: when an incremental stream flips to
3696 // Error mid-prepare, the RFC 9218 §4 yield-after-one
3697 // accounting must drop this stream from the
3698 // same-urgency ready bucket so trailing peers see the
3699 // live count.
3700 let freshly_rst_post_prepare =
3701 kawa.is_error() && self.stream_table.rst_sent_mut().insert(stream_id);
3702 if freshly_rst_post_prepare {
3703 // Deferred to after the loop; same reason as the
3704 // pre-prepare collector above.
3705 pass.freshly_emitted_rsts.push(rst_error_from_kawa(kawa));
3706 pass.census_mut().note_ineligible(urgency, is_incremental);
3707 }
3708 *parts.window = parts.window.saturating_sub(pass.consumed);
3709 self.flow_control.consume_send_window(pass.consumed);
3710 let consumed = pass.consumed;
3711 pass.census_mut()
3712 .note_fired(urgency, stream_id, is_incremental, consumed);
3713 }
3714 #[cfg(debug_assertions)]
3715 debug_assert!(
3716 !never_opened_error || kawa.out.len() == out_before,
3717 "nothing is encoded for a backend stream in error that never opened"
3718 );
3719 context.debug.push(DebugEvent::S(
3720 stream_id,
3721 global_stream_id,
3722 kawa.parsing_phase,
3723 kawa.blocks.len(),
3724 kawa.out.len(),
3725 ));
3726 pass.stream_bytes = 0;
3727 pass.phase = H2WritePhase::Flush {
3728 cursor,
3729 stream_id,
3730 gid: global_stream_id,
3731 stream_state,
3732 urgency,
3733 is_incremental,
3734 };
3735 }
3736 H2WritePhase::Flush {
3737 cursor,
3738 stream_id,
3739 gid: global_stream_id,
3740 stream_state,
3741 urgency,
3742 is_incremental,
3743 } => {
3744 let write_stream = H2StreamId::Other {
3745 id: stream_id,
3746 gid: global_stream_id,
3747 };
3748 // THE ROUND-AGAIN: the pre-image's
3749 // `while !kawa.out.is_empty()`, re-expressed as a resumption
3750 // state over the same stream. `pass.stalled` is the only
3751 // terminator, and `update_readiness` sets it on `size == 0`
3752 // alone — a write that moved bytes under `WouldBlock` comes
3753 // straight back here for another round.
3754 if !pass.stalled {
3755 let kawa = write_buffer(
3756 &mut self.zero,
3757 &mut context.streams,
3758 &self.position,
3759 write_stream,
3760 );
3761 // A write that cut this stream's last frame left its
3762 // rest in the output queue and `kawa.out` empty: that
3763 // rest goes now, not after another stream's frames.
3764 if !kawa.out.is_empty() || !self.output.is_empty() {
3765 // The pre-image raised this flag at the top of every
3766 // round of the SCHEDULER loop's flush and never in
3767 // the resume path's, which passed `None` for it.
3768 pass.socket_write = true;
3769 return H2WriteTarget::Transmit {
3770 stream_id: write_stream,
3771 };
3772 }
3773 }
3774 let stream = &mut context.streams[global_stream_id];
3775 let parts = stream.split(&self.position);
3776 let kawa = parts.wbuffer;
3777 // Refresh the per-stream idle timer on outbound bytes. Without
3778 // this, a long-running response trickled at low bandwidth would
3779 // be killed by `cancel_timed_out_streams` mid-delivery — the
3780 // inbound-only refreshes in `handle_data_frame` (non-empty DATA)
3781 // and `handle_headers_frame` never fire while the peer is idle.
3782 if pass.stream_bytes > 0 {
3783 self.stream_table.touch_activity(stream_id, self.now);
3784 }
3785 // Arm/age the dedicated flow-control-stall deadline that catches a
3786 // window-stalled stream — a buffered RESPONSE to a slow frontend
3787 // (`Position::Server`) OR a buffered request UPLOAD to a slow H2
3788 // backend (`Position::Client`): window-stall reaping is bidirectional
3789 // by design (M4), so there is no position gate here. Set only when the
3790 // stream holds sendable buffered data it cannot send because its
3791 // effective send window is exhausted; unlike `stream_last_activity_at`
3792 // it is NEVER refreshed by inbound DATA/HEADERS, so a peer dribbling
3793 // 1-byte DATA cannot keep it warm.
3794 //
3795 // M2 cumulative-stall budget: a genuinely OPEN window clears the
3796 // deadline immediately (real un-stall). While the window stays
3797 // blocked, accumulate this pass's outbound drain; only cumulative
3798 // progress reaching `FC_STALL_CLEAR_FLOOR` (a full frame of real
3799 // delivery) clears it. A `WINDOW_UPDATE(+1)` drip drains ~1 byte/pass
3800 // straight back to a zero window, so it never reaches the floor — the
3801 // deadline ages out and `cancel_timed_out_streams` RST(CANCEL)s the
3802 // slot-pinning stream after `stream_idle_timeout`.
3803 let outbound_window_blocked = has_sendable_response(kawa)
3804 && min(*parts.window, self.flow_control.window()) <= 0
3805 && (!kawa.blocks.is_empty() || !kawa.out.is_empty());
3806 match fc_stall_budget_decision(
3807 outbound_window_blocked,
3808 pass.consumed,
3809 self.stream_table.fc_stall_progress(stream_id),
3810 ) {
3811 FcStallAction::Clear => {
3812 self.stream_table.clear_fc_stall(stream_id);
3813 }
3814 FcStallAction::Arm { progress } => {
3815 self.stream_table
3816 .arm_fc_stall(stream_id, self.now, progress);
3817 }
3818 }
3819 pass.total_bytes_written =
3820 pass.total_bytes_written.saturating_add(pass.stream_bytes);
3821 if pass.stalled {
3822 self.stream_table.set_expect_write(Some(write_stream));
3823 self.parked_header_block = h2_transmit::holds_header_frame(kawa);
3824 self.parked_data = parked_data_credit(kawa);
3825 pass.phase = H2WritePhase::End;
3826 continue;
3827 }
3828 self.stream_table.set_expect_write(None);
3829 if (kawa.is_terminated() || kawa.is_error())
3830 && kawa.is_completed()
3831 && !Self::handle_1xx_reset(kawa, stream_state, endpoint)
3832 {
3833 // `keep_alive_frontend` is not read here: a default
3834 // answer's `Connection: close` is an HTTP/1.1
3835 // connection option, stripped from H2 (RFC 9113
3836 // §8.2.2). The answer ends its own stream only.
3837 let (client_rtt, server_rtt) =
3838 self.snapshot_rtts(endpoint, stream.linked_token());
3839
3840 if let Some((dead_id, token)) = self.try_recycle_server_stream(
3841 stream,
3842 global_stream_id,
3843 stream_id,
3844 pass.byte_totals,
3845 &mut context.debug,
3846 context.listener.clone(),
3847 client_rtt,
3848 server_rtt,
3849 ) {
3850 pass.completed_streams
3851 .push((dead_id, global_stream_id, token));
3852 // LIFECYCLE §9 invariant 17: leave the census INSIDE
3853 // the scheduler loop so later streams see the reduced
3854 // count. The post-loop retirement at remove_dead_stream
3855 // is too late.
3856 pass.census_mut().note_ineligible(urgency, is_incremental);
3857 }
3858 }
3859 pass.phase = H2WritePhase::Prepare { cursor: cursor + 1 };
3860 }
3861 H2WritePhase::Ended => {
3862 // The pass already answered. `write_streams` never gets
3863 // here — it returns on `Done` and on `Finalize` — but this
3864 // function is `pub`, and `End` below takes the
3865 // scheduler-pass values out as its first statement, so a
3866 // second poll re-entering that arm would `expect` on three
3867 // empty `Option`s. Answering `Done` keeps a hand-driven
3868 // caller on the ordinary terminator instead of a panic.
3869 return H2WriteTarget::Done(MuxResult::Continue);
3870 }
3871 H2WritePhase::End => {
3872 // FIRST statement of the arm, before any `return` it can
3873 // take: it leaves the pass holding `None` for the rest of
3874 // its life. The phase moves to `Ended` in the same
3875 // breath so a re-entry cannot reach this statement twice.
3876 let (converter_pass, order, census) = pass.release_scheduler_pass();
3877 pass.phase = H2WritePhase::Ended;
3878 // Sample the pass's final bucket totals. Publication is deferred to
3879 // the `gauge_connection_state` call below because the value must
3880 // reach the gauge in the SAME pass that computed it — the entry call
3881 // in `begin_scheduler_pass` runs before the pass census exists,
3882 // so sampling there would publish the previous pass's value.
3883 self.ready_incremental_streams = census.ready_total();
3884 // RFC 7541 §6.3: the pass hands back the signal no header block
3885 // carried. It is `None` once a block did, and unchanged after a
3886 // DATA-only pass, so the signal stays queued for the next pass
3887 // with a header block. A block dropped for its size re-arms it
3888 // with a table reset (`H2BlockConverter::check_header_capacity`),
3889 // which must survive the pass even when an earlier block of the
3890 // same pass carried the old signal.
3891 self.pending_table_size_update = converter_pass.pending_table_size_update();
3892 // End the pass and take its three reusable buffers back. They are
3893 // moved, not copied: the pass never owned an allocation of its own.
3894 let (converter_out, lowercase_buf, cookie_buf) = converter_pass.into_buffers();
3895 // Publish `ready_incremental_streams` (and any window/stream drift the
3896 // pass produced) before the early return below, so no pass
3897 // samples without emitting.
3898 self.gauge_connection_state();
3899 // Account every RST that the converter emitted during this pass
3900 // (pre-prepare gate + post-prepare HPACK over-budget abort) so
3901 // the global tx counter and the per-error breakdown stay in
3902 // step. These resets are `RstOrigin::Local` — what errored is
3903 // the message from the other side of the proxy, not a frame
3904 // this connection's peer sent — so they never feed the
3905 // MadeYouReset cap; the `Done` arm stays for the contract of
3906 // `Self::account_emitted_rst`.
3907 //
3908 // The SECOND terminator that must not finalize. It also sits
3909 // between `into_buffers` and the `put_*` calls, so a cap trip
3910 // drops the three buffers — the pre-image's behaviour, kept
3911 // deliberately: the connection is sending a GOAWAY.
3912 for error in std::mem::take(&mut pass.freshly_emitted_rsts) {
3913 if let Some(result) = self.account_emitted_rst(error, RstOrigin::Local) {
3914 return H2WriteTarget::Done(result);
3915 }
3916 }
3917 self.hpack.put_converter_buf(converter_out);
3918 self.hpack.put_lowercase_buf(lowercase_buf);
3919 self.hpack.put_cookie_buf(cookie_buf);
3920 self.hpack.shrink_converter_buffers();
3921 // RFC 9218 §4: end the pass — take the order buffer back and commit
3922 // the round-robin cursor so the next writable cycle begins with the
3923 // stream immediately after the one we fired first this pass
3924 // (LIFECYCLE §9 invariant 26). Placed here, after the deferred RST
3925 // accounting above, so a MadeYouReset cap trip that returns a GOAWAY
3926 // early skips both exactly as it did before the inversion.
3927 self.scheduler.end_pass(order, census);
3928 for (dead_id, global_stream_id, token) in
3929 std::mem::take(&mut pass.completed_streams)
3930 {
3931 // Retirement is deferred out of the loop on purpose, and this is
3932 // an ORDERING requirement rather than a borrow workaround:
3933 // `try_recycle_server_stream` passes `is_last_stream` as
3934 // `streams().len() == 1`, and that branch hands the WHOLE
3935 // remaining connection overhead pool to one stream instead of a
3936 // proportional share. Retiring inline would let a later completer
3937 // of the same pass read `len() == 1` while other streams are still
3938 // live, and drain the pool early. (`active_streams` is passed
3939 // too, but it is only the even-split divisor of the fallback
3940 // branch, reached while the connection-wide byte total is still
3941 // zero — on every other pass the divisor is that total.)
3942 // Retiring here also runs
3943 // before `endpoint.end_stream()` can trigger teardown and observe
3944 // a stale `Recycle` entry in `self.stream_table.streams()`.
3945 self.remove_dead_stream(dead_id, global_stream_id);
3946 if let Some(token) = token {
3947 remove_backend_stream(
3948 &mut context.backend_streams,
3949 token,
3950 global_stream_id,
3951 );
3952 endpoint.end_stream(token, global_stream_id, context);
3953 }
3954 }
3955 return H2WriteTarget::Finalize {
3956 socket_write: pass.socket_write,
3957 bytes_written: pass.total_bytes_written,
3958 };
3959 }
3960 }
3961 }
3962 }
3963
3964 /// Open the scheduler half of one write pass: the gauge sample, the
3965 /// converter, the RFC 9218 ordering and its census.
3966 ///
3967 /// Called once per pass, from [`H2WritePhase::Start`] when nothing is
3968 /// parked and from [`H2WritePhase::Resume`] once the parked stream drained.
3969 /// It is NOT part of `H2WritePass::new`, and the reason is behavioural
3970 /// rather than stylistic: the resume path can retire a stream through
3971 /// [`Self::remove_dead_stream`], and `H2Scheduler::begin_pass` enumerates
3972 /// the same `stream_table.streams()` map — so building the order at pass
3973 /// start would change which streams the pass visits. The converter is
3974 /// excluded for a different reason again: its constructor `mem::take`s
3975 /// three scratch buffers out of [`hpack_state::HpackState`], and the
3976 /// resume path's stall ends the pass before anything hands them back.
3977 fn begin_scheduler_pass<L>(&mut self, context: &mut Context<L>, pass: &mut H2WritePass)
3978 where
3979 L: ListenerHandler + L7ListenerHandler,
3980 {
3981 self.gauge_connection_state();
3982
3983 let scheme: &'static [u8] = if context.protocol == Protocol::HTTPS {
3984 b"https"
3985 } else {
3986 b"http"
3987 };
3988 // The converter is built for ONE `kawa.prepare` call at a time (see
3989 // [`converter::H2ConverterPass`]), so its encoder borrow never spans
3990 // the per-stream loop and every `&self` / `&mut self` method stays
3991 // callable inside it. The pass carries the three reusable scratch
3992 // buffers — moved, never copied — plus the RFC 7541 §6.3 pending
3993 // size-update, so the first header block of this pass prepends the
3994 // signal and no later one repeats it. `H2WritePhase::End` takes back
3995 // whatever signal the pass has left (`pending_table_size_update()`),
3996 // so a DATA-only write pass (no header block) does not drop it.
3997 let converter_pass = converter::H2ConverterPass::new(
3998 self.peer_settings.settings_max_frame_size as usize,
3999 scheme,
4000 // When this connection is a backend client we are writing
4001 // toward the upstream backend — flow-control stalls in that
4002 // direction are scoped to `backend.flow_control.paused` (in
4003 // addition to the existing direction-agnostic
4004 // `h2.flow_control_stall`).
4005 self.position.is_client(),
4006 self.hpack.take_converter_buf(),
4007 self.hpack.take_lowercase_buf(),
4008 self.hpack.take_cookie_buf(),
4009 self.pending_table_size_update,
4010 );
4011 // The whole RFC 9218 ordering decision — ascending urgency, stream
4012 // id for stability, incremental streams to the tail of their bucket,
4013 // that tail rotated by the round-robin cursor — plus the pass's
4014 // same-urgency ready-peer census, belongs to
4015 // [`h2_scheduler::H2Scheduler`]. It runs over state that module
4016 // owns; the ONE fact it cannot own is whether a stream has anything
4017 // to send, which it takes as this closure. The closure is called for
4018 // incremental streams only, so it costs exactly what the inline
4019 // census cost. `order` is returned by value for the same reason the
4020 // scratch buffers are moved into the converter pass: the per-stream
4021 // phases below re-borrow the encoder out of `self.hpack` for every
4022 // eligible stream, so no borrow of a connection field may span them.
4023 let is_server = matches!(self.position, Position::Server);
4024 let (order, census) =
4025 self.scheduler
4026 .begin_pass(self.stream_table.streams().keys().copied(), |stream_id| {
4027 let Some(&gid) = self.stream_table.streams().get(&stream_id) else {
4028 return false;
4029 };
4030 let wbuffer = if is_server {
4031 &context.streams[gid].back
4032 } else {
4033 &context.streams[gid].front
4034 };
4035 wbuffer.is_main_phase()
4036 || (wbuffer.is_terminated() && !wbuffer.is_completed())
4037 || (wbuffer.is_error() && !self.stream_table.rst_sent_contains(stream_id))
4038 });
4039
4040 trace!(
4041 "{} PRIORITIES: {:?} (incremental_count={}, per_bucket={:?})",
4042 log_context!(self),
4043 order,
4044 census.incremental_count(),
4045 census.ready_buckets()
4046 );
4047 pass.adopt_scheduler_pass(converter_pass, order, census);
4048 pass.phase = H2WritePhase::Prepare { cursor: 0 };
4049 }
4050
4051 /// Tell the core what the caller's write actually did.
4052 ///
4053 /// The second half of [`Self::poll_write_target`]'s protocol, and the
4054 /// pre-image flush loop's BODY minus the three statements the shell now
4055 /// owns: [`h2_transmit::gather`] has already run, the write has happened,
4056 /// and [`h2_transmit::confirm`] has already discharged the `'static`
4057 /// descriptors, so nothing here holds a borrow of `kawa.storage`.
4058 ///
4059 /// It answers nothing, because nothing in this body can end a pass: the
4060 /// pass continues after a transmit, and the decision to stop is taken by
4061 /// [`Self::poll_write_target`] reading `H2WritePass::stalled`.
4062 ///
4063 /// **This is the only place on the write core that sees a
4064 /// [`SocketResult`]**, and the only place that writes `pass.stalled` — in
4065 /// one statement, the verbatim move of the pre-image's flush test.
4066 /// `update_readiness_after_write` returns stalled **iff `size == 0`**;
4067 /// `status` only clears the event bit. `FrontRustls` answers
4068 /// `(buffered_size > 0, WouldBlock)` structurally whenever rustls accepted
4069 /// plaintext while the kernel was full, and that is NOT a stall.
4070 ///
4071 /// Which counter the bytes land in, and which `debug_site` they are logged
4072 /// under, are decided by the PASS PHASE and not by the stream: the resume
4073 /// path's bytes are socket catch-up rather than the voluntary scheduler
4074 /// yield LIFECYCLE §9 invariant 16 retains `Ready::WRITABLE` for, so
4075 /// `H2WritePass::resume_bytes` must never reach `finalize_write`.
4076 pub fn handle_write<L>(
4077 &mut self,
4078 context: &mut Context<L>,
4079 stream_id: H2StreamId,
4080 written: H2Written,
4081 status: SocketResult,
4082 pass: &mut H2WritePass,
4083 ) where
4084 L: ListenerHandler + L7ListenerHandler,
4085 {
4086 self.note_write_status(status);
4087 let H2Written {
4088 size,
4089 from_output,
4090 tail,
4091 } = written;
4092 // The queue went first. Its own bytes are connection overhead, and
4093 // `consume_output` restores reading if they were holding it back.
4094 if from_output > 0 {
4095 self.consume_output(from_output);
4096 }
4097 // The stream's share: what the socket took past the queue, plus the
4098 // unsent rest of the frame it cut, which the queue adopts now and
4099 // sends ahead of anything queued later (#1604). That rest is counted
4100 // to the stream here, since it is committed and the stream may be
4101 // gone by the time the queue sends it.
4102 if tail > 0 {
4103 let kawa = write_buffer(
4104 &mut self.zero,
4105 &mut context.streams,
4106 &self.position,
4107 stream_id,
4108 );
4109 self.output.adopt_tail(kawa, tail);
4110 }
4111 let committed = size - from_output + tail;
4112 let resuming = matches!(pass.phase, H2WritePhase::Resume { .. });
4113 // `2` for the resume path and `3` for the scheduler loop, the two
4114 // `debug_site` values the pre-image passed from its two call sites.
4115 let debug_site = if resuming { 2 } else { 3 };
4116 let cross_read_amount = match pass.phase {
4117 H2WritePhase::Resume {
4118 cross_read_amount, ..
4119 } => cross_read_amount,
4120 _ => None,
4121 };
4122 let global_stream_id = match stream_id {
4123 H2StreamId::Zero => usize::MAX,
4124 H2StreamId::Other {
4125 gid: global_stream_id,
4126 ..
4127 } => global_stream_id,
4128 };
4129 context
4130 .debug
4131 .push(DebugEvent::SocketIO(debug_site, global_stream_id, size));
4132 let event = self.position.bytes_out_event(committed);
4133 self.metric_events.push(event);
4134 if let H2StreamId::Other {
4135 gid: global_stream_id,
4136 ..
4137 } = stream_id
4138 {
4139 self.position
4140 .count_bytes_out(&mut context.streams[global_stream_id].metrics, committed);
4141 if self.position.is_server() {
4142 context.client_bytes_out = context.client_bytes_out.wrapping_add(committed);
4143 }
4144 context.stream_bytes_out = context.stream_bytes_out.wrapping_add(committed);
4145 // LIFECYCLE §9 invariant 9, write side. Outbound APPLICATION data
4146 // is activity; an acknowledgement is not. Both halves of this
4147 // condition are load-bearing and neither is implied by reaching
4148 // `H2Shell::write_streams`: an `H2StreamId::Other` id is what
4149 // makes these bytes a stream's rather than `self.zero`'s, and
4150 // `size > 0` is what makes them bytes the socket actually took.
4151 // `arm_timeout` reads `self.now`, the snapshot `writable()` adopted
4152 // at pass entry, so arming from here lands on the same instant the
4153 // top of the pass would have computed — the gate is the whole
4154 // change, not the moment.
4155 if committed > 0 {
4156 self.arm_timeout();
4157 }
4158 }
4159 if resuming {
4160 pass.resume_bytes = pass.resume_bytes.saturating_add(committed);
4161 } else {
4162 pass.stream_bytes = pass.stream_bytes.saturating_add(committed);
4163 }
4164 if let Some(amount) = cross_read_amount {
4165 // Resume path: same stream is parked waiting for buffer space.
4166 // Re-enable READABLE once the write freed enough room.
4167 let kawa = write_buffer(
4168 &mut self.zero,
4169 &mut context.streams,
4170 &self.position,
4171 stream_id,
4172 );
4173 if kawa.storage.available_space() >= amount {
4174 self.readiness.interest.insert(Ready::READABLE);
4175 }
4176 }
4177 pass.stalled = update_readiness_after_write(size, status, &mut self.readiness);
4178 }
4179
4180 /// After forwarding a 1xx informational response (100 Continue, 103 Early
4181 /// Hints), reset the back buffer and re-enable backend readable so the
4182 /// final response can arrive on the same stream. Returns true if the
4183 /// response was 1xx.
4184 fn handle_1xx_reset<E: Endpoint>(
4185 kawa: &mut GenericHttpStream,
4186 stream_state: StreamState,
4187 endpoint: &mut E,
4188 ) -> bool {
4189 let is_1xx = matches!(
4190 kawa.detached.status_line,
4191 kawa::StatusLine::Response { code, .. } if (100..200).contains(&code)
4192 );
4193 if !is_1xx {
4194 return false;
4195 }
4196 debug!(
4197 "{} H2 write_streams: 1xx informational forwarded, resetting back buffer",
4198 log_module_context!()
4199 );
4200 kawa.clear();
4201 if let StreamState::Linked(token) = stream_state {
4202 let readiness = endpoint.readiness_mut(token);
4203 readiness.interest.insert(Ready::READABLE);
4204 readiness.signal_pending_read();
4205 }
4206 true
4207 }
4208
4209 /// Re-arm edge-triggered WRITABLE event if rustls still has buffered TLS data.
4210 ///
4211 /// Takes the answer rather than asking for it: `tls_wants_write` is
4212 /// `H2Shell::tls_wants_write`'s result, read by the caller. That query was
4213 /// this composite's whole remaining socket reach, and handing it in is
4214 /// what leaves a body a byte-in / byte-out core can keep.
4215 ///
4216 /// When the caller reads it is not uniform, and cannot be. Five of the six
4217 /// call sites already hold the answer — four from the `h2_close` decision
4218 /// just taken on it, [`H2Shell::initiate_close_notify`] from its own branch
4219 /// condition — and pass that binding: nothing mutates the socket in
4220 /// between, so a second query would answer the same. The sixth, the
4221 /// [`H2ControlFlushTarget::Stalled`] arm of [`H2Shell::writable`], has no
4222 /// such binding and must read it AFTER the `H2Shell::flush_output_to_socket`
4223 /// whose stall brought it there: that write is what changes the answer.
4224 /// The stalled-drain tails of [`Self::flush_pending_control_frames`] used
4225 /// to read it and re-arm individually; its one output flush now answers
4226 /// [`H2ControlFlushTarget::Stalled`] and that arm re-arms.
4227 ///
4228 /// No re-arm follows a write the kernel refused
4229 /// (`kernel_refused_write`): the socket is full, the kernel raises
4230 /// the next WRITABLE edge once the peer reads, and that edge flushes the
4231 /// records. A synthetic edge would only repeat the refused write, and
4232 /// `Mux::ready_inner` did so on every inner iteration until
4233 /// `MAX_LOOP_ITERATIONS` counted `http.infinite_loop.error` whenever a
4234 /// TLS client stopped reading (sozu-proxy/sozu#1788). The close decisions
4235 /// still read `tls_wants_write` unchanged: a connection with records
4236 /// pending keeps WRITABLE interest and stays open until they are flushed.
4237 pub fn ensure_tls_flushed(&mut self, tls_wants_write: bool) {
4238 if tls_wants_write && !self.kernel_refused_write {
4239 self.readiness.signal_pending_write();
4240 }
4241 }
4242
4243 /// Record what a socket write of this pass answered, for
4244 /// [`Self::ensure_tls_flushed`]. Every write the shell makes reports
4245 /// here, so the flag always describes the latest one.
4246 ///
4247 /// A refused write also consumes the WRITABLE event, as
4248 /// `update_readiness_after_write` does for the writes whose status it
4249 /// reads. The empty-buffer flushes of `H2Shell::flush_tls_records` have
4250 /// no other reader of their status, so without this a refused flush left
4251 /// the edge it was called on in place, and `Mux::ready_inner` ran the
4252 /// pass again on every inner iteration.
4253 fn note_write_status(&mut self, status: SocketResult) {
4254 self.kernel_refused_write = status == SocketResult::WouldBlock;
4255 if self.kernel_refused_write {
4256 self.readiness.event.remove(Ready::WRITABLE);
4257 }
4258 }
4259
4260 /// Evict every per-stream piece of state carried by this `ConnectionH2`.
4261 ///
4262 /// **Invariant**: `stream_table` (wire map, `rst_sent`,
4263 /// `stream_last_activity_at`, `stream_fc_stalled_since`,
4264 /// `stream_fc_stalled_progress`, `expect_read`/`expect_write`) and
4265 /// `prioriser` MUST be emptied of `stream_id` here. `prioriser` is the
4266 /// only per-stream cache not folded into [`h2_stream_table::H2StreamTable`]
4267 /// — this module deliberately does not have the RFC 9218 priority map,
4268 /// same boundary [`h2_flow_control`]'s doc draws for per-stream send
4269 /// window state. `H2StreamTable::remove` asserts its own five caches are
4270 /// clean as a postcondition; forgetting `prioriser` here would still
4271 /// cause unbounded memory growth on long-lived connections with many
4272 /// cancelled streams, so it stays a second, explicit call.
4273 ///
4274 /// When the stream owns the DATA payload being read, the rest of that
4275 /// payload is skipped instead of left unread — see
4276 /// [`Self::skip_orphaned_data_payload`].
4277 fn remove_dead_stream(&mut self, stream_id: StreamId, global_stream_id: GlobalStreamId) {
4278 // Read before `H2StreamTable::remove` nulls `expect_write`: the
4279 // parked DATA dies with its stream (sozu-proxy/sozu#1641).
4280 if matches!(
4281 self.stream_table.expect_write(),
4282 Some(H2StreamId::Other { gid, .. }) if gid == global_stream_id
4283 ) {
4284 let parked_data = std::mem::take(&mut self.parked_data);
4285 if parked_data > 0 {
4286 // An overflow sends GOAWAY(FLOW_CONTROL_ERROR); its outcome
4287 // lives on in the connection state (see the callee's doc).
4288 let _ = self.refund_parked_data(parked_data);
4289 }
4290 }
4291 // Read before `H2StreamTable::remove` nulls `expect_read`.
4292 let orphaned = match (self.stream_table.expect_read(), &self.state) {
4293 (Some((H2StreamId::Other { gid, .. }, remaining)), H2State::Frame(header))
4294 if gid == global_stream_id =>
4295 {
4296 Some((remaining, header.payload_len))
4297 }
4298 _ => None,
4299 };
4300 if self.stream_table.remove(stream_id, global_stream_id)
4301 == h2_stream_table::RemoveOutcome::NotPresent
4302 {
4303 error!(
4304 "{} dead stream_id {} missing from streams map",
4305 log_context!(self),
4306 stream_id
4307 );
4308 }
4309 self.scheduler.remove_stream(&stream_id);
4310 if let Some((remaining, payload_len)) = orphaned {
4311 self.skip_orphaned_data_payload(remaining, payload_len);
4312 }
4313 }
4314
4315 /// Give the connection send window back `dropped` octets of DATA credit
4316 /// spent on frames that were prepared and then dropped before the wire
4317 /// took any of them ([`Self::parked_data`], sozu-proxy/sozu#1641), and
4318 /// re-arm WRITABLE when that reopens the window: every stream starved on
4319 /// it may have released WRITABLE, and nothing else would wake them. The
4320 /// stream's own window is the caller's, since only a live stream has one
4321 /// worth restoring.
4322 ///
4323 /// When the peer's WINDOW_UPDATEs have meanwhile pushed the window so
4324 /// high that the refund would pass 2^31-1, the credit the peer granted
4325 /// overflows (RFC 9113 §6.9.1) and this answers the GOAWAY
4326 /// (FLOW_CONTROL_ERROR) `Self::handle_window_update_frame` sends for an
4327 /// overflowing increment. `Self::goaway` records its outcome in the
4328 /// connection state, so a caller that cannot return it
4329 /// ([`Self::remove_dead_stream`]) may drop the result.
4330 fn refund_parked_data(&mut self, dropped: i32) -> Option<MuxResult> {
4331 debug_assert!(dropped > 0, "only a positive refund is applied");
4332 let window_before = self.flow_control.window();
4333 match self.flow_control.refund_send_window(dropped) {
4334 h2_flow_control::ApplyWindowUpdateOutcome::Applied {
4335 new_window,
4336 should_arm_writable,
4337 } => {
4338 if should_arm_writable {
4339 self.readiness.arm_writable();
4340 }
4341 debug_assert!(
4342 new_window > window_before,
4343 "an applied refund grew the connection window"
4344 );
4345 debug!(
4346 "{} H2 prepared DATA dropped unsent: {} octets of send credit given back, connection window {}",
4347 log_context!(self),
4348 dropped,
4349 new_window
4350 );
4351 None
4352 }
4353 h2_flow_control::ApplyWindowUpdateOutcome::Overflow => {
4354 debug_assert_eq!(
4355 self.flow_control.window(),
4356 window_before,
4357 "an overflowing refund is not applied"
4358 );
4359 error!(
4360 "{} INVALID WINDOW INCREMENT: refunding {} octets of dropped DATA passes 2^31-1",
4361 log_context!(self),
4362 dropped
4363 );
4364 Some(self.goaway(H2Error::FlowControlError))
4365 }
4366 }
4367 }
4368
4369 /// Empty the encoder's dynamic table after a header block it encoded was
4370 /// dropped unsent, and queue the size updates `0`, then the maximum size,
4371 /// for the next block, which empty the peer's table as well (RFC 7541
4372 /// §4.2; sozu-proxy/sozu#1627). The caller guarantees that no block
4373 /// encoded before this call is still queued for the wire.
4374 fn reset_encoder_table(&mut self) {
4375 let max_size = self.hpack.reset_encoder_table();
4376 let signal = u32::try_from(max_size).unwrap_or(u32::MAX);
4377 debug_assert_eq!(
4378 usize::try_from(signal).ok(),
4379 Some(max_size),
4380 "the encoder's maximum size came from a u32 setting"
4381 );
4382 self.pending_table_size_update = Some(signal);
4383 debug!(
4384 "{} H2 an encoded header block was dropped unsent: HPACK encoder table reset, next block signals 0 then {}",
4385 log_context!(self),
4386 max_size
4387 );
4388 debug_assert!(
4389 self.pending_table_size_update.is_some(),
4390 "the reset is signalled on the next header block"
4391 );
4392 }
4393
4394 /// Keep the connection reading after the stream that owned the DATA
4395 /// payload being read was removed mid-frame (idle reap, reset).
4396 ///
4397 /// Nulling `expect_read` alone left the connection in `H2State::Frame`
4398 /// with nothing to read, so it never read again (#1597). The remaining
4399 /// `remaining` bytes are read and dropped through `H2State::Discard`,
4400 /// in pieces as small as the room in `zero` allows, like HAProxy's
4401 /// "skipping remaining frame payload". `handle_data_frame` will never
4402 /// see this frame, so its whole `payload_len`, padding included, is
4403 /// credited to the connection receive window here (RFC 9113 §6.9), as
4404 /// its closed-stream branch does. READABLE interest is restored because
4405 /// the stream may have been parked on buffer pressure, and
4406 /// `try_resume_reading` only resumes a stream that still exists.
4407 fn skip_orphaned_data_payload(&mut self, remaining: usize, payload_len: u32) {
4408 let conn_threshold = self.connection_config.initial_connection_window / 2;
4409 if let Some(increment) = self
4410 .flow_control
4411 .account_received_bytes(payload_len, conn_threshold)
4412 {
4413 self.queue_window_update(0, increment);
4414 self.readiness.arm_writable();
4415 }
4416 self.state = H2State::Discard;
4417 self.discarded_field_block = None;
4418 self.stream_table
4419 .set_expect_read(Some((H2StreamId::Zero, remaining)));
4420 self.readiness.interest.insert(Ready::READABLE);
4421 }
4422
4423 /// Whether the read in progress is an orphaned DATA remainder that is
4424 /// dropped as it is read ([`Self::skip_orphaned_data_payload`]). A
4425 /// refused HEADERS discard always carries a field block.
4426 fn skipping_orphaned_payload(&self) -> bool {
4427 matches!(self.state, H2State::Discard) && self.discarded_field_block.is_none()
4428 }
4429
4430 /// Drop stream-id mappings for streams that never became active before a
4431 /// connection-level close. This happens on incomplete/oversized header
4432 /// blocks: the stream slot is created on the initial HEADERS frame, then a
4433 /// GOAWAY closes the connection before the request is fully materialized.
4434 pub fn prune_inactive_streams_while_closing<L>(&mut self, context: &mut Context<L>)
4435 where
4436 L: ListenerHandler + L7ListenerHandler,
4437 {
4438 if !self.drain.draining() || !matches!(self.state, H2State::GoAway | H2State::Error) {
4439 return;
4440 }
4441
4442 let stale_streams = self
4443 .stream_table
4444 .streams()
4445 .iter()
4446 .filter_map(|(&stream_id, &global_stream_id)| {
4447 (!context.streams[global_stream_id].state.is_open())
4448 .then_some((stream_id, global_stream_id))
4449 })
4450 .collect::<Vec<_>>();
4451
4452 for (stream_id, global_stream_id) in stale_streams {
4453 let stream = &mut context.streams[global_stream_id];
4454 if stream.state == StreamState::Idle {
4455 stream.front.clear();
4456 stream.front.storage.clear();
4457 stream.back.clear();
4458 stream.back.storage.clear();
4459 stream.metrics.reset();
4460 stream.state = StreamState::Recycle;
4461 }
4462 self.remove_dead_stream(stream_id, global_stream_id);
4463 }
4464 }
4465
4466 /// Post-write phase: check drain completion, flush TLS, and update readiness.
4467 ///
4468 /// `bytes_written_this_pass` reports the total outbound bytes `write_streams`
4469 /// pushed to the socket (across every stream), and is used to distinguish
4470 /// two very different "no `expect_write`" states:
4471 ///
4472 /// - **Voluntary yield with progress**: at least one DATA/HEADERS frame
4473 /// emitted, but a stream left non-empty `back.out`/`back.blocks` because
4474 /// the converter yielded (e.g. RFC 9218 incremental rotation). LIFECYCLE
4475 /// §9 invariant 16: keep `Ready::WRITABLE` armed so the session loop can
4476 /// resume flushing on the next tick without waiting for an external
4477 /// wake-up that edge-triggered epoll will not deliver.
4478 /// - **No progress at all**: converter pushed every block back (e.g. flow
4479 /// window exhausted, no HEADERS ready yet). Strip `Ready::WRITABLE` —
4480 /// forward progress must come from an external trigger
4481 /// (`WINDOW_UPDATE`, new request), not from looping writable().
4482 ///
4483 /// Answers [`H2FinalizeTarget`]: [`H2FinalizeTarget::Done`] in the normal
4484 /// case — `MuxResult::Continue`, or the graceful GOAWAY taken when
4485 /// draining with every stream gone — and otherwise the TLS flush step its
4486 /// caller must perform before [`Self::finalize_write_after_flush`] settles
4487 /// the pass.
4488 ///
4489 /// `tls_wants_write` is `H2Shell::tls_wants_write`'s answer, read by the
4490 /// caller. It is the pre-flush half of the query / flush / query triple
4491 /// this function used to run inline; the flush and the post-flush query
4492 /// are the caller's too, which is what leaves a body a byte-in /
4493 /// byte-out core can keep. The caller reads it before the RFC 9113 §6.8
4494 /// graceful-GOAWAY check below rather than after, which the query's
4495 /// freedom from side effects makes unobservable: on that path the value
4496 /// is discarded.
4497 pub fn finalize_write<L>(
4498 &mut self,
4499 tls_wants_write: bool,
4500 socket_write: bool,
4501 bytes_written_this_pass: usize,
4502 context: &mut Context<L>,
4503 ) -> H2FinalizeTarget
4504 where
4505 L: ListenerHandler + L7ListenerHandler,
4506 {
4507 // RFC 9113 §6.8: if draining and all streams have completed,
4508 // send the final GOAWAY with the actual last_stream_id
4509 if self.drain.draining() && self.stream_table.streams().is_empty() {
4510 return H2FinalizeTarget::Done(self.graceful_goaway(self.now));
4511 }
4512
4513 // The flush triple and the readiness policy behind it are one
4514 // decision, taken in `h2_close` where it is enumerable without a
4515 // socket. Why it is a sibling of `CloseAction` rather than four more
4516 // of its variants, why the conditional middle flush is an INPUT
4517 // instead of an `if` kept here, and why the invariant-16 probe is
4518 // passed as a closure: `h2_close::finalize_action`.
4519 // Bound rather than matched inline: the invariant-16 probe below
4520 // borrows `self.stream_table` and `context.streams`, and a `match`
4521 // keeps its scrutinee's temporaries alive for every arm — including
4522 // the arms that need `&mut self.readiness` and `&mut context.debug`.
4523 let action = h2_close::finalize_action(
4524 TlsFlushPhase::BeforeFlush,
4525 tls_wants_write,
4526 socket_write,
4527 self.stream_table.expect_write().is_some(),
4528 bytes_written_this_pass > 0,
4529 || any_stream_has_pending_back(self.stream_table.streams(), &context.streams),
4530 self.control_tx.has_pending()
4531 || !self.flow_control.pending_window_updates_is_empty()
4532 || !self.output.is_empty(),
4533 );
4534 match action {
4535 // A parked `expect_write` owns the next tick: no bit moves.
4536 FinalizeAction::Parked => H2FinalizeTarget::Done(MuxResult::Continue),
4537 // LIFECYCLE §9 invariant 16: a voluntary scheduler yield left
4538 // stranded bytes in a stream's `back.out`/`back.blocks` after a
4539 // pass that made forward progress. Retaining `Ready::WRITABLE` is
4540 // the ABSENCE of the withdrawal below, so this arm changes no bit
4541 // — it only narrates.
4542 FinalizeAction::RetainPendingBack => {
4543 #[cfg(debug_assertions)]
4544 context.debug.push(DebugEvent::Str(
4545 "finalize_write: invariant 16 retained WRITABLE (pending back-buffer)"
4546 .to_owned(),
4547 ));
4548 H2FinalizeTarget::Done(MuxResult::Continue)
4549 }
4550 // Control-frame liveness: a pass that ended before
4551 // `flush_pending_control_frames` drained the RST / WINDOW_UPDATE
4552 // queues, or with bytes still in the ordered output queue, must
4553 // keep `Ready::WRITABLE`. Without this rearm the next tick would
4554 // drop it and the queued frame would stall until an unrelated
4555 // event re-triggered writable — which is exactly the scenario
4556 // h2spec trips by sending back-to-back malformed streams. A
4557 // stalled output flush parks the pass before it gets here, so
4558 // the output leg is defence in depth (#1604).
4559 FinalizeAction::ArmControlQueue => {
4560 #[cfg(debug_assertions)]
4561 context.debug.push(DebugEvent::Str(
4562 "finalize_write: retained WRITABLE (control queue non-empty)".to_owned(),
4563 ));
4564 self.readiness.arm_writable();
4565 self.metric_events
4566 .push(MetricEvent::WritableRearmedControlQueue);
4567 H2FinalizeTarget::Done(MuxResult::Continue)
4568 }
4569 // We wrote everything.
4570 FinalizeAction::Quiesce => {
4571 #[cfg(debug_assertions)]
4572 context.debug.push(DebugEvent::Str(format!(
4573 "Wrote everything: {:?}",
4574 self.stream_table.streams()
4575 )));
4576 self.readiness.interest.remove(Ready::WRITABLE);
4577 H2FinalizeTarget::Done(MuxResult::Continue)
4578 }
4579 // The two TLS answers differ only in the step the CALLER performs,
4580 // and both come back through the single post-flush query in
4581 // `Self::finalize_write_after_flush`.
4582 FinalizeAction::Flush => H2FinalizeTarget::Flush,
4583 FinalizeAction::SkipFlush => H2FinalizeTarget::SkipFlush,
4584 // Named rather than `other =>`: a wildcard arm would turn a new
4585 // `FinalizeAction` variant into a release-mode panic on the proxy
4586 // write path instead of a compile error.
4587 action @ (FinalizeAction::ReArm | FinalizeAction::Settled) => {
4588 unreachable!("BeforeFlush yielded {action:?}")
4589 }
4590 }
4591 }
4592
4593 /// Settle a write pass whose [`H2FinalizeTarget`] asked for the TLS flush
4594 /// step, given the answer `H2Shell::tls_wants_write` gave AFTER it.
4595 ///
4596 /// The second half of [`Self::finalize_write`]'s protocol, and the reason
4597 /// the split is not a rename: `tls_wants_write` here is a DIFFERENT
4598 /// question from the one that produced [`H2FinalizeTarget::Flush`] or
4599 /// [`H2FinalizeTarget::SkipFlush`]. The first asks whether rustls holds
4600 /// records; this one asks whether the step between them landed —
4601 /// `socket_write(&[])`'s `(size, status)` is discarded at the call site,
4602 /// so nothing else on this path can tell. A caller that reused the first
4603 /// answer here would re-arm forever or never, and either one strands the
4604 /// connection under edge-triggered epoll.
4605 ///
4606 /// The readiness policy of [`Self::finalize_write`] is deliberately NOT
4607 /// reopened: once records were found pending, this pass leaves the bits to
4608 /// the next one, so the degenerate arguments below are the shape
4609 /// `h2_close::goaway_close_action`'s own post-flush call already uses —
4610 /// the inputs the flush could not change are not re-read.
4611 pub fn finalize_write_after_flush(&mut self, tls_wants_write: bool) -> MuxResult {
4612 let action = h2_close::finalize_action(
4613 TlsFlushPhase::AfterFlush,
4614 tls_wants_write,
4615 false,
4616 false,
4617 false,
4618 || false,
4619 false,
4620 );
4621 match action {
4622 // The post-flush answer is read once, by the caller, and feeds
4623 // both the decision and the re-arm: they are one question asked at
4624 // one point, and nothing mutates the socket between them. The
4625 // re-arm still goes through `ensure_tls_flushed` rather than
4626 // touching `signal_pending_write` here, which keeps every
4627 // post-decision TLS re-arm in this file spelled the same way, as
4628 // the GoAway and Error arms in `dispatch_writable_state` and
4629 // `dispatch_writable_state_after_flush` already do.
4630 FinalizeAction::ReArm => self.ensure_tls_flushed(tls_wants_write),
4631 FinalizeAction::Settled => {}
4632 action @ (FinalizeAction::Flush
4633 | FinalizeAction::SkipFlush
4634 | FinalizeAction::Parked
4635 | FinalizeAction::RetainPendingBack
4636 | FinalizeAction::ArmControlQueue
4637 | FinalizeAction::Quiesce) => {
4638 unreachable!("AfterFlush yielded {action:?}")
4639 }
4640 }
4641 MuxResult::Continue
4642 }
4643
4644 /// Queue pending control frames (WINDOW_UPDATEs, RST_STREAMs) behind the
4645 /// ordered output and flush it, before entering the main writable state
4646 /// machine.
4647 ///
4648 /// Answers [`H2ControlFlushTarget`]: [`H2ControlFlushTarget::Proceed`]
4649 /// when `writable()` should carry on into its state dispatch,
4650 /// [`H2ControlFlushTarget::Done`] when a GOAWAY already ended the pass,
4651 /// [`H2ControlFlushTarget::FlushOutput`] when [`Self::output_pending`]
4652 /// holds bytes for the caller to move, and
4653 /// [`H2ControlFlushTarget::Stalled`] when that move stalled — in which
4654 /// case the caller reads `tls_wants_write` and hands it to
4655 /// [`Self::ensure_tls_flushed`].
4656 ///
4657 /// The socket write is the caller's, as everywhere on this write path:
4658 /// the core answers a step, the caller performs the socket work, and the
4659 /// core is told what the socket said, through
4660 /// [`Self::handle_control_flush`].
4661 ///
4662 /// There is one flush, at the end. Every stage appends whole frames to
4663 /// the one ordered output queue (#1604), so a stage no longer has to wait
4664 /// for an earlier one's bytes to leave a shared buffer before it can
4665 /// serialise, and whatever was already queued — an ACK, the rest of a
4666 /// stream frame a partial write cut — stays ahead of what this walk adds.
4667 pub fn flush_pending_control_frames(&mut self) -> H2ControlFlushTarget {
4668 // The peer hung up while the connection drains: nothing queued for it
4669 // will be read. Only OUTPUT is dropped. `zero` is input only, and a
4670 // `Ready::HUP` can be a TCP half-close with frames the peer already
4671 // sent still unread (review of e1c3c2fb, B2), which the shutdown
4672 // drive's forced `readable()` goes on to read.
4673 if self.frontend_hung_up_while_draining() {
4674 self.stream_table.set_expect_write(None);
4675 self.clear_output();
4676 self.flow_control.clear_pending_window_updates();
4677 self.control_tx.clear_pending();
4678 }
4679
4680 // RFC 9113 §6.5: check if peer has timed out on SETTINGS ACK
4681 if let Some(sent_at) = self.settings_sent_at
4682 && self.now.saturating_duration_since(sent_at) >= SETTINGS_ACK_TIMEOUT
4683 {
4684 warn!(
4685 "{} SETTINGS ACK timeout: no SETTINGS ACK observed within {:?}",
4686 log_context!(self),
4687 SETTINGS_ACK_TIMEOUT
4688 );
4689 return H2ControlFlushTarget::Done(self.goaway(H2Error::SettingsTimeout));
4690 }
4691
4692 // Stage — queue pending WINDOW_UPDATE frames.
4693 // Queued inline to avoid extra event loop iterations that could cause
4694 // response data to be sent before subsequent frames are validated.
4695 let pending = self.flow_control.pending_window_updates_len();
4696 if pending > 0 {
4697 let flow_control = &mut self.flow_control;
4698 let mut frames_written = 0;
4699 // Emission order is the deterministic ascending stream_id order
4700 // `H2FlowControl` guarantees — see its module doc — not
4701 // insertion/arrival order.
4702 let _ = self
4703 .output
4704 .push_frames(pending * WINDOW_UPDATE_FRAME_SIZE, |buf| {
4705 let (offset, frames) = flow_control.drain_window_updates_into(buf);
4706 frames_written = frames;
4707 Ok::<_, std::convert::Infallible>(offset)
4708 });
4709 if frames_written > 0 {
4710 self.metric_events
4711 .push(MetricEvent::WindowUpdateFramesSent(frames_written as i64));
4712 }
4713 }
4714
4715 // Stage — RST_STREAM overflow check + queue.
4716 // The pending queue is bounded; an RST it had to refuse never reaches
4717 // the wire, so the peer would believe that stream still live. Escalate
4718 // before serialising anything. This is a bound on what is pending, not
4719 // a lifetime cap: resets drained as they come never trip it, and the
4720 // peer-provoked ones are capped by the flood detector instead.
4721 if !matches!(self.state, H2State::GoAway | H2State::Error) && self.control_tx.overflowed() {
4722 error!(
4723 "{} pending RST_STREAM queue overflowed its bound {} ({} queued over the connection), sending GOAWAY(ENHANCE_YOUR_CALM)",
4724 log_context!(self),
4725 self.control_tx.max_pending(),
4726 self.control_tx.lifetime_queued(),
4727 );
4728 return H2ControlFlushTarget::Done(self.goaway(H2Error::EnhanceYourCalm));
4729 }
4730
4731 // Queue pending RST_STREAM frames (queued when refusing streams).
4732 // Accounting happens at queue-time inside `Self::enqueue_rst`, so this
4733 // only serialises — no metric/flood calls here would double-count.
4734 // Emission order is queue order, which is arrival order over
4735 // already-deduped stream ids — see `h2_control_tx`'s module doc.
4736 let pending = self.control_tx.pending_len();
4737 if pending > 0 {
4738 let control_tx = &mut self.control_tx;
4739 let _ = self
4740 .output
4741 .push_frames(pending * RST_STREAM_FRAME_SIZE, |buf| {
4742 let (offset, _frames_written) = control_tx.drain_rst_streams_into(buf);
4743 Ok::<_, std::convert::Infallible>(offset)
4744 });
4745 }
4746
4747 if self.output.is_empty() {
4748 H2ControlFlushTarget::Proceed
4749 } else {
4750 H2ControlFlushTarget::FlushOutput
4751 }
4752 }
4753
4754 /// Answer the [`H2ControlFlushTarget::FlushOutput`] the caller just
4755 /// performed: `stalled` is what the caller's write reported.
4756 pub fn handle_control_flush(&mut self, stalled: bool) -> H2ControlFlushTarget {
4757 if stalled {
4758 H2ControlFlushTarget::Stalled
4759 } else {
4760 H2ControlFlushTarget::Proceed
4761 }
4762 }
4763
4764 /// Take `writable()`'s `(H2State, Position)` decision, given the answer
4765 /// `H2Shell::tls_wants_write` gave AFTER the pass preamble's flush.
4766 ///
4767 /// `tls_wants_write` is that answer, read by the caller. It is the third
4768 /// step of the preamble's own query / flush / query triple and the
4769 /// pre-flush question both close arms decide on: the
4770 /// `(H2State::Error, Position::Server)` arm has no flush of its own and
4771 /// reads nothing else, while the `H2State::GoAway` arm asks for a second
4772 /// flush through [`H2WritableStateTarget::Flush`]. ONE read serves
4773 /// whichever of the two runs — only one of them can — and nothing mutates
4774 /// the socket between the caller's read and this call. The arms that never
4775 /// consult it discard it, exactly as `H2Shell::write_streams` discards the
4776 /// answer it reads before [`Self::finalize_write`]'s graceful-GOAWAY
4777 /// check; `H2Shell::tls_wants_write` is free of side effects, which is what
4778 /// makes both unobservable.
4779 ///
4780 /// Answers [`H2WritableStateTarget`]. Every arm but two ends the pass with
4781 /// [`H2WritableStateTarget::Done`]; the `H2State::GoAway` arm may ask for
4782 /// the TLS flush step, and the four proxying states name
4783 /// [`H2WritableStateTarget::WriteStreams`]. That last one is the caller's
4784 /// to run for the same reason the flush is — `H2Shell::write_streams` owns
4785 /// the socket and this function does not — and it is why this function
4786 /// takes neither `Context` nor `Endpoint`: no other arm reads either.
4787 pub fn dispatch_writable_state(&mut self, tls_wants_write: bool) -> H2WritableStateTarget {
4788 match (&self.state, &self.position) {
4789 (H2State::Error, Position::Server) => {
4790 // The caller's preamble already attempted this pass's flush,
4791 // so this arm decides on the post-flush answer and has no
4792 // `Flush` of its own — see `h2_close`'s module doc.
4793 match h2_close::error_close_action(tls_wants_write) {
4794 CloseAction::ReArmAndContinue => {
4795 self.ensure_tls_flushed(tls_wants_write);
4796 H2WritableStateTarget::Done(MuxResult::Continue)
4797 }
4798 CloseAction::CloseSession => {
4799 H2WritableStateTarget::Done(MuxResult::CloseSession)
4800 }
4801 // Named rather than `other =>`: a wildcard arm would turn a
4802 // new `CloseAction` variant into a release-mode panic in the
4803 // proxy write path instead of a compile error.
4804 action @ (CloseAction::Flush | CloseAction::Disconnect) => {
4805 unreachable!("error_close_action yielded {action:?}")
4806 }
4807 }
4808 }
4809 // The preface and SETTINGS are out and the server's SETTINGS are
4810 // not read yet: there is nothing to write until they are. A write
4811 // readiness here is not an error — `start_stream` arms one for a
4812 // stream linked to this connection, which `Router::plan_connect`'s
4813 // pool scan does for a `Connected` connection whatever its handshake
4814 // state — so it is withdrawn, and the SETTINGS ACK the read queues
4815 // re-arms it (sozu-proxy/sozu#1631). Only the control-frame preamble,
4816 // which already ran, may have written: stream frames wait for `Header`.
4817 (H2State::ServerSettings, Position::Client(..)) => {
4818 trace!(
4819 "{} waiting for the server SETTINGS before writing streams",
4820 log_context!(self)
4821 );
4822 self.readiness.interest.remove(Ready::WRITABLE);
4823 debug_assert!(
4824 !self.readiness.interest.is_writable(),
4825 "a backend awaiting the server SETTINGS must stop asking to write"
4826 );
4827 debug_assert!(
4828 self.readiness.interest.is_readable(),
4829 "a backend awaiting the server SETTINGS must keep reading them"
4830 );
4831 H2WritableStateTarget::Done(MuxResult::Continue)
4832 }
4833 (H2State::Error, _) | (H2State::ClientSettings, Position::Server) => {
4834 error!(
4835 "{} Unexpected combination: (Writable, {:?}, {:?})",
4836 log_context!(self),
4837 self.state,
4838 self.position
4839 );
4840 H2WritableStateTarget::Done(self.force_disconnect())
4841 }
4842 (H2State::ClientPreface, Position::Server) => {
4843 H2WritableStateTarget::Done(MuxResult::Continue)
4844 }
4845 // Discard state: pending data (e.g. RST_STREAM) was already
4846 // written in the caller's control-frame preamble; let the readable
4847 // path consume the remaining frame payload.
4848 (H2State::Discard, _) => H2WritableStateTarget::Done(MuxResult::Continue),
4849 // A stream parked by a stalled write still has whole frames
4850 // queued. The rest of the frame the stall cut, if any, already
4851 // went with the GOAWAY from the ordered output (#1604); finish
4852 // the others before the close decision, since a GOAWAY does not
4853 // close the streams at or below its last stream id (RFC 9113
4854 // §6.8).
4855 (H2State::GoAway, _)
4856 if matches!(
4857 self.stream_table.expect_write(),
4858 Some(H2StreamId::Other { .. })
4859 ) =>
4860 {
4861 H2WritableStateTarget::WriteStreams
4862 }
4863 (H2State::GoAway, _) => {
4864 // Response DATA frames may still sit in rustls's output
4865 // buffer — accepted by socket_write_vectored during
4866 // write_streams() but not yet flushed to TCP. Under TCP
4867 // backpressure (HAProxy chain) this is the primary truncation
4868 // vector, so the decision lives in `h2_close`, exhaustively
4869 // unit-tested there rather than inline here. Why the two
4870 // `socket_wants_write()` queries are two DIFFERENT questions,
4871 // and why the flush between them stays inside the same
4872 // `writable()` call: see `h2_close::TlsFlushPhase`. The flush
4873 // and the query after it belong to the caller, and
4874 // `H2WritableStateTarget::Flush` is how this arm asks for them.
4875 match h2_close::goaway_close_action(
4876 TlsFlushPhase::BeforeFlush,
4877 self.peer_gone_after_final_goaway(),
4878 tls_wants_write,
4879 ) {
4880 CloseAction::CloseSession => {
4881 H2WritableStateTarget::Done(MuxResult::CloseSession)
4882 }
4883 CloseAction::Flush => H2WritableStateTarget::Flush,
4884 CloseAction::Disconnect => H2WritableStateTarget::Done(self.force_disconnect()),
4885 action @ CloseAction::ReArmAndContinue => {
4886 unreachable!("BeforeFlush yielded {action:?}")
4887 }
4888 }
4889 }
4890 (H2State::ClientPreface, Position::Client(..)) => {
4891 trace!("{} Preparing preface and settings", log_context!(self));
4892 // RFC 9113 §3.4: the preface is the first byte on the wire.
4893 // No stream frame can be queued before it, because no stream
4894 // opens before `Header` (sozu-proxy/sozu#1631).
4895 debug_assert!(
4896 self.output.is_empty(),
4897 "nothing may be queued ahead of the client connection preface"
4898 );
4899 self.output.push(serializer::H2_PRI.as_bytes());
4900 let local_settings = &self.local_settings;
4901 match self.output.push_frames(SETTINGS_FRAME_SIZE, |buf| {
4902 serializer::gen_settings(buf, local_settings).map(|(_, size)| size)
4903 }) {
4904 Ok(_) => {
4905 self.metric_events.push(MetricEvent::SettingsFrameSent);
4906 // RFC 9113 §6.5: start tracking SETTINGS ACK timeout
4907 self.settings_sent_at = Some(self.now);
4908 }
4909 Err(error) => {
4910 error!(
4911 "{} Could not serialize SettingsFrame: {:?}",
4912 log_context!(self),
4913 error
4914 );
4915 return H2WritableStateTarget::Done(self.force_disconnect());
4916 }
4917 };
4918
4919 self.state = H2State::ClientSettings;
4920 self.readiness.signal_pending_write();
4921 H2WritableStateTarget::Done(MuxResult::Continue)
4922 }
4923 (H2State::ClientSettings, Position::Client(..)) => {
4924 trace!("{} Sent preface and settings", log_context!(self));
4925 self.state = H2State::ServerSettings;
4926 self.stream_table
4927 .set_expect_read(Some((H2StreamId::Zero, 9)));
4928 // A backend connection starts write-only
4929 // (`Connection::new_h2_client`); it reads from here on, for
4930 // the server's SETTINGS. The preamble's output flush used to
4931 // turn READABLE on as a side effect of draining `zero`.
4932 self.readiness.interest.insert(Ready::READABLE);
4933 self.readiness.interest.remove(Ready::WRITABLE);
4934 H2WritableStateTarget::Done(MuxResult::Continue)
4935 }
4936 (H2State::ServerSettings, Position::Server) => {
4937 // The connection-window enlargement already left with the
4938 // SETTINGS it follows: the `(ClientSettings, Server)` readable
4939 // arm serialises both into `zero`, which the preamble above
4940 // drained in one write.
4941 self.expect_header();
4942 H2WritableStateTarget::Done(MuxResult::Continue)
4943 }
4944 // Proxying states — writing application data (request/response).
4945 // These arms are NOT a filter on what the pass emits: a connection
4946 // whose only queued output is a PING or SETTINGS acknowledgement is
4947 // in `H2State::Header` too, so reaching `write_streams` never meant
4948 // "application data is being written". The frontend timeout is
4949 // therefore armed by `ConnectionH2::handle_write`, per stream
4950 // transmit that moved bytes, and not on the way in here.
4951 (H2State::Header, _)
4952 | (H2State::Frame(_), _)
4953 | (H2State::ContinuationFrame(_), _)
4954 | (H2State::ContinuationHeader(_), _) => H2WritableStateTarget::WriteStreams,
4955 }
4956 }
4957
4958 /// Settle the `H2State::GoAway` arm whose [`H2WritableStateTarget::Flush`]
4959 /// asked for the TLS flush step, given the answer
4960 /// `H2Shell::tls_wants_write` gave AFTER it.
4961 ///
4962 /// The second half of [`Self::dispatch_writable_state`]'s protocol, built
4963 /// the way [`Self::finalize_write_after_flush`] is built and for the same
4964 /// reason: `tls_wants_write` here is a DIFFERENT question from the one
4965 /// that arm decided on. The first asks whether rustls holds records; this
4966 /// one asks whether the flush between them landed —
4967 /// `socket_write(&[])`'s `(size, status)` is discarded at the call site,
4968 /// so nothing else on this path can tell. A caller that reused the first
4969 /// answer here would close a TLS connection with records still pending,
4970 /// and the peer would read a truncated response; that is the truncation
4971 /// vector `h2_close`'s module doc names, not a style point.
4972 ///
4973 /// `peer_gone` is deliberately not re-read — the degenerate `false` below
4974 /// is `h2_close::goaway_close_action`'s own post-flush convention — for
4975 /// the same reason [`Self::finalize_write_after_flush`] does not reopen
4976 /// the readiness policy: the flush cannot resurrect a departed peer, and
4977 /// the decision answers from the one input it could have changed.
4978 pub fn dispatch_writable_state_after_flush(&mut self, tls_wants_write: bool) -> MuxResult {
4979 match h2_close::goaway_close_action(TlsFlushPhase::AfterFlush, false, tls_wants_write) {
4980 CloseAction::ReArmAndContinue => {
4981 self.ensure_tls_flushed(tls_wants_write);
4982 MuxResult::Continue
4983 }
4984 CloseAction::Disconnect => self.force_disconnect(),
4985 // Named rather than `other =>`: a wildcard arm would turn a new
4986 // `CloseAction` variant into a release-mode panic in the proxy
4987 // write path instead of a compile error.
4988 action @ (CloseAction::CloseSession | CloseAction::Flush) => {
4989 unreachable!("AfterFlush yielded {action:?}")
4990 }
4991 }
4992 }
4993
4994 /// Report the access-log RTTs for the local frontend and the linked backend.
4995 ///
4996 /// Both cells are asked of `endpoint`, at the moment the stream is logged
4997 /// and at no other time. `client_rtt` comes from [`Endpoint::local_rtt`]:
4998 /// `ShellEndpoint` samples the frontend socket at the first stream logged
4999 /// on the connection and hands every later stream that one value, so
5000 /// every stream of a connection reports one number (issue #1339 Q11
5001 /// asked for one per pass), and a connection that logs nothing reads
5002 /// nothing (issue #1590). `server_rtt` comes from [`Endpoint::peer_rtt`],
5003 /// the backend connection's own one sample. `doc/configure.md` states
5004 /// both for operators reading the log.
5005 ///
5006 /// Nothing in this method reaches a socket, which is the point: the local
5007 /// RTT was the last `SocketHandler::socket_ref` read in this file, and the
5008 /// shell that owns the socket answers it now.
5009 ///
5010 /// `Position::Server`-only. On a backend H2 connection (`Position::Client`)
5011 /// the report would write swapped values onto the shared `Stream.metrics`:
5012 /// `Mux` samples the frontend into the frontend connection alone, and the
5013 /// corresponding `EndpointServer::peer_rtt` returns that same frontend, so
5014 /// the per-stream `client_rtt`/`server_rtt` cells would be populated with
5015 /// mislabelled values. Gating keeps backend H2 from poisoning the
5016 /// access-log metric for the matching frontend stream.
5017 ///
5018 /// Callers must invoke this BEFORE `endpoint.end_stream(...)` on reset
5019 /// paths so the backend lookup does not depend on
5020 /// `EndpointClient::end_stream` continuing to leave entries in
5021 /// `Router.backends`.
5022 fn snapshot_rtts<E: Endpoint>(
5023 &self,
5024 endpoint: &E,
5025 linked_token: Option<mio::Token>,
5026 ) -> (Option<Duration>, Option<Duration>) {
5027 if !self.position.is_server() {
5028 return (None, None);
5029 }
5030 (
5031 endpoint.local_rtt(),
5032 linked_token.and_then(|t| endpoint.peer_rtt(t)),
5033 )
5034 }
5035
5036 /// Try to recycle a completed server-side stream by distributing overhead,
5037 /// generating access logs, and transitioning the stream to `Recycle` state.
5038 ///
5039 /// Returns `Some((stream_id, Option<token>))` if the stream was recycled, so the
5040 /// caller can add `stream_id` to the dead-streams list and call `endpoint.end_stream()`
5041 /// if a token was returned. Returns `None` if recycling was deferred or not applicable.
5042 ///
5043 /// `client_rtt`/`server_rtt` are snapshotted by the caller — which must
5044 /// do so BEFORE `endpoint.end_stream(...)`, see [`Self::snapshot_rtts`] —
5045 /// and forwarded into the access log. `stream` stays a parameter rather
5046 /// than being looked up here because the caller already holds it as
5047 /// `&mut context.streams[global_stream_id]`.
5048 #[allow(clippy::too_many_arguments)]
5049 fn try_recycle_server_stream<L>(
5050 &mut self,
5051 stream: &mut crate::protocol::mux::Stream,
5052 global_stream_id: GlobalStreamId,
5053 stream_id: StreamId,
5054 byte_totals: (usize, usize),
5055 debug: &mut DebugHistory,
5056 listener: std::rc::Rc<std::cell::RefCell<L>>,
5057 client_rtt: Option<Duration>,
5058 server_rtt: Option<Duration>,
5059 ) -> Option<(StreamId, Option<mio::Token>)>
5060 where
5061 L: ListenerHandler + L7ListenerHandler,
5062 {
5063 match self.position {
5064 Position::Client(..) => None,
5065 Position::Server => {
5066 // Already logged by a reset path; retire the stream after its RST is flushed.
5067 if stream.metrics.start.is_none() {
5068 let state = std::mem::replace(&mut stream.state, StreamState::Recycle);
5069 return match state {
5070 StreamState::Linked(token) => Some((stream_id, Some(token))),
5071 _ => Some((stream_id, None)),
5072 };
5073 }
5074
5075 // Don't recycle if the client hasn't sent END_STREAM yet —
5076 // more DATA frames may arrive for this stream.
5077 if !stream.front_received_end_of_stream {
5078 trace!(
5079 "{} Defer recycle stream {}: client still sending",
5080 log_module_context!(),
5081 global_stream_id
5082 );
5083 return None;
5084 }
5085 let stream_bytes = (
5086 stream.metrics.bin + stream.metrics.backend_bin,
5087 stream.metrics.bout + stream.metrics.backend_bout,
5088 );
5089 let active_streams = self.stream_table.streams().len();
5090 distribute_overhead(
5091 &mut stream.metrics,
5092 &mut self.bytes.overhead_bin,
5093 &mut self.bytes.overhead_bout,
5094 stream_bytes,
5095 byte_totals,
5096 active_streams,
5097 active_streams == 1,
5098 );
5099 debug.push(DebugEvent::StreamEvent(4, global_stream_id));
5100 trace!(
5101 "{} Recycle stream: {}",
5102 log_module_context!(),
5103 global_stream_id
5104 );
5105 self.metric_events.push(MetricEvent::EndToEndH2Request);
5106 if routed_to_a_backend(stream) {
5107 self.flood_detector.record_stream_opened();
5108 }
5109 let (token, events) =
5110 Self::complete_server_stream(stream, listener, client_rtt, server_rtt);
5111 self.metric_events.extend(events.into_iter().flatten());
5112 Some((stream_id, token))
5113 }
5114 }
5115 }
5116
5117 /// Finalize a server-side stream after its response has been fully written.
5118 ///
5119 /// Generates an access log, resets metrics, and transitions the stream to `Recycle`.
5120 /// Returns the backend token if the stream was `Linked`, so the caller can call
5121 /// `endpoint.end_stream()` with the full `Context` (which can't be passed here
5122 /// because `stream` borrows from `context.streams`).
5123 ///
5124 /// Callers must distribute overhead *before* calling this: it resets
5125 /// `stream.metrics`, so a share credited afterwards would be discarded.
5126 fn complete_server_stream<L>(
5127 stream: &mut crate::protocol::mux::Stream,
5128 listener: std::rc::Rc<std::cell::RefCell<L>>,
5129 client_rtt: Option<Duration>,
5130 server_rtt: Option<Duration>,
5131 ) -> (Option<mio::Token>, [Option<MetricEvent>; 2])
5132 where
5133 L: ListenerHandler + L7ListenerHandler,
5134 {
5135 stream.metrics.backend_stop();
5136 let events = stream.generate_access_log(
5137 false,
5138 Some("H2::Complete"),
5139 listener,
5140 client_rtt,
5141 server_rtt,
5142 );
5143 stream.metrics.reset();
5144 let state = std::mem::replace(&mut stream.state, StreamState::Recycle);
5145 let token = if let StreamState::Linked(token) = state {
5146 Some(token)
5147 } else {
5148 None
5149 };
5150 (token, events)
5151 }
5152
5153 /// Compute the total bytes transferred across all active streams.
5154 ///
5155 /// Returns `(total_bytes_in, total_bytes_out)` where bytes_in = `bin + backend_bin`
5156 /// and bytes_out = `bout + backend_bout` for each stream.
5157 pub fn compute_stream_byte_totals<L: ListenerHandler + L7ListenerHandler>(
5158 &self,
5159 context: &Context<L>,
5160 ) -> (usize, usize) {
5161 let mut total_in = 0usize;
5162 let mut total_out = 0usize;
5163 for &gid in self.stream_table.streams().values() {
5164 let m = &context.streams[gid].metrics;
5165 total_in += m.bin + m.backend_bin;
5166 total_out += m.bout + m.backend_bout;
5167 }
5168 (total_in, total_out)
5169 }
5170
5171 /// Distribute connection-level byte overhead proportionally to a single stream.
5172 ///
5173 /// `totals` should be pre-computed via [`compute_stream_byte_totals`] **before**
5174 /// taking a mutable borrow on the target stream, to avoid borrow conflicts.
5175 /// Delegates to the free function [`distribute_overhead`].
5176 fn distribute_overhead(&mut self, metrics: &mut SessionMetrics, totals: (usize, usize)) {
5177 let stream_bytes = (
5178 metrics.bin + metrics.backend_bin,
5179 metrics.bout + metrics.backend_bout,
5180 );
5181 distribute_overhead(
5182 metrics,
5183 &mut self.bytes.overhead_bin,
5184 &mut self.bytes.overhead_bout,
5185 stream_bytes,
5186 totals,
5187 self.stream_table.streams().len(),
5188 self.stream_table.streams().len() <= 1,
5189 );
5190 }
5191
5192 /// Attribute accumulated `zero_bytes_read` to the stream or to connection overhead.
5193 fn attribute_bytes_to_stream(&mut self, metrics: &mut SessionMetrics) {
5194 self.position
5195 .count_bytes_in(metrics, self.bytes.zero_bytes_read);
5196 self.bytes.zero_bytes_read = 0;
5197 }
5198
5199 fn attribute_bytes_to_overhead(&mut self) {
5200 self.bytes.overhead_bin += self.bytes.zero_bytes_read;
5201 self.bytes.zero_bytes_read = 0;
5202 }
5203
5204 /// Queue a WINDOW_UPDATE, coalescing with any existing entry for the same stream_id.
5205 /// RFC 9113 §6.9.1: window size increment MUST be 1..2^31-1 (0x7FFFFFFF);
5206 /// `increment == 0` is a legal no-op to *send* and queues nothing (see
5207 /// `h2_flow_control::H2FlowControl::queue_window_update`).
5208 ///
5209 /// Always signals pending write so callers don't have to remember the
5210 /// edge-triggered epoll invariant:
5211 /// under ET epoll a queued WINDOW_UPDATE without a live WRITABLE event bit
5212 /// is invisible to filter_interest() and will never get flushed.
5213 fn queue_window_update(&mut self, stream_id: u32, increment: u32) {
5214 match self.flow_control.queue_window_update(
5215 stream_id,
5216 increment,
5217 self.max_pending_window_updates,
5218 ) {
5219 h2_flow_control::QueueWindowUpdateOutcome::Coalesced { old, new } => {
5220 trace!(
5221 "{} WINDOW_UPDATE coalesced: stream={} old={} new={}",
5222 log_context!(self),
5223 stream_id,
5224 old,
5225 new
5226 );
5227 }
5228 h2_flow_control::QueueWindowUpdateOutcome::Inserted { increment } => {
5229 trace!(
5230 "{} WINDOW_UPDATE queued: stream={} increment={}",
5231 log_context!(self),
5232 stream_id,
5233 increment
5234 );
5235 }
5236 h2_flow_control::QueueWindowUpdateOutcome::Dropped => {
5237 error!(
5238 "{} WINDOW_UPDATE dropped: queue full ({} entries), stream={} increment={}",
5239 log_context!(self),
5240 self.max_pending_window_updates,
5241 stream_id,
5242 increment
5243 );
5244 self.metric_events.push(MetricEvent::WindowUpdateDropped);
5245 }
5246 // Zero increment: nothing was queued, nothing to log.
5247 h2_flow_control::QueueWindowUpdateOutcome::Noop => {}
5248 }
5249 self.readiness.arm_writable();
5250 }
5251
5252 /// Re-enable READABLE if this connection is parked waiting for buffer space
5253 /// and the target stream's buffer now has enough room.
5254 ///
5255 /// This is the cross-readiness counterpart to the same-connection check in
5256 /// `writable()`. When the *other side* of a stream (frontend or backend)
5257 /// drains data via its own `writable()`, it frees buffer space that this
5258 /// connection was waiting for. Without this explicit wake-up the connection
5259 /// stays parked and the session deadlocks until a timeout fires.
5260 ///
5261 /// Returns `true` if READABLE was re-enabled.
5262 pub fn try_resume_reading<L>(&mut self, context: &Context<L>) -> bool
5263 where
5264 L: ListenerHandler + L7ListenerHandler,
5265 {
5266 if let Some((
5267 H2StreamId::Other {
5268 gid: global_stream_id,
5269 ..
5270 },
5271 amount,
5272 )) = self.stream_table.expect_read()
5273 {
5274 let stream = &context.streams[global_stream_id];
5275 let kawa = match self.position {
5276 Position::Client(..) => &stream.back,
5277 Position::Server => &stream.front,
5278 };
5279 if kawa.storage.available_space() >= amount {
5280 self.readiness.interest.insert(Ready::READABLE);
5281 return true;
5282 }
5283 }
5284 false
5285 }
5286
5287 /// Mark a stream's position-appropriate end-of-stream flag.
5288 ///
5289 /// Server reads from the front (client), so sets `front_received_end_of_stream`.
5290 /// Client reads from the back (backend), so sets `back_received_end_of_stream`.
5291 fn mark_end_of_stream(&self, stream: &mut crate::protocol::mux::Stream) {
5292 if self.position.is_server() {
5293 stream.front_received_end_of_stream = true;
5294 } else {
5295 stream.back_received_end_of_stream = true;
5296 }
5297 }
5298
5299 /// Cancel streams that have been idle longer than [`Self::stream_idle_timeout`].
5300 ///
5301 /// A stream is considered idle when no meaningful application data (non-empty
5302 /// DATA frames or HEADERS) has been received since the last activity timestamp
5303 /// in `H2StreamTable::stream_last_activity_at`.
5304 ///
5305 /// Mitigates slow-multiplex Slowloris (Pass 4 Medium #3): the connection-level
5306 /// idle timer resets on every frame, so a peer sending periodic control frames
5307 /// can pin `max_concurrent_streams` slots for the full nominal connection timeout.
5308 /// Per-stream idle deadlines guarantee each stream terminates if it stops making
5309 /// forward progress, regardless of connection-level liveness.
5310 ///
5311 /// Timed-out streams receive RST_STREAM(CANCEL) and are immediately removed
5312 /// from the streams map so they no longer count against MAX_CONCURRENT_STREAMS.
5313 /// Backend endpoints are notified and metrics are finalized.
5314 pub fn cancel_timed_out_streams<E, L>(&mut self, context: &mut Context<L>, endpoint: &mut E)
5315 where
5316 E: Endpoint,
5317 L: ListenerHandler + L7ListenerHandler,
5318 {
5319 // Entry point: `Mux::timeout` reaches this directly, without going
5320 // through `readable`, so adopt the mux's snapshot here too.
5321 self.now = context.now;
5322 // Per-connection scratch Vecs (`converter_buf`, `lowercase_buf`,
5323 // `cookie_buf`, and the scheduler's own pass-order buffer) grow to a
5324 // high-water mark and never shrink. On a long-lived idle H2
5325 // connection that briefly carried a flurry of large headers, the
5326 // backing memory stays pinned indefinitely. Reclaim past
5327 // `SCRATCH_BUF_RETAIN` when the connection has live streams but
5328 // each scratch buffer holds 4× the cap. Quiet-time only — runs
5329 // at the top of every `cancel_timed_out_streams` invocation
5330 // (which is itself called from the readable hot loop, but only
5331 // on a session that has been idle long enough to risk timing
5332 // out a stream).
5333 const SCRATCH_BUF_RETAIN: usize = 16 * 1024;
5334 self.hpack.reclaim_idle_buffers(SCRATCH_BUF_RETAIN);
5335 self.scheduler.reclaim_idle_buffer(SCRATCH_BUF_RETAIN);
5336
5337 if self.stream_table.is_empty()
5338 || (self.stream_table.activity_is_empty() && self.stream_table.fc_stall_is_empty())
5339 {
5340 return;
5341 }
5342 let now = self.now;
5343 let deadline = self.stream_idle_timeout;
5344 // Two independent per-stream guards reap on the same deadline — see
5345 // `H2StreamTable::collect_timed_out`. The flow-control-stall guard
5346 // (`stream_fc_stalled_since`) closes the HTTP/2 window-stall vector that
5347 // the bidirectional liveness guard (`stream_last_activity_at`) misses,
5348 // because an inbound DATA drip keeps the liveness timer warm while the
5349 // response stays window-blocked.
5350 let timed_out = self.stream_table.collect_timed_out(now, deadline);
5351 if timed_out.is_empty() {
5352 return;
5353 }
5354 for (sid, reason) in timed_out {
5355 info!(
5356 "{} H2 stream {} exceeded {:?} ({}), cancelling",
5357 log_context!(self),
5358 sid,
5359 deadline,
5360 reason
5361 );
5362 // M1: break reaps down by guard so a window-stall reap (a DoS
5363 // mitigation) is distinguishable from an ordinary idle reap on a
5364 // dashboard. M2: a window-stall reap whose stream dribbled some
5365 // outbound progress (`acc > 0`) below the floor is specifically a
5366 // stall-budget reap — the `WINDOW_UPDATE`-drip vector the budget
5367 // closes — counted as a subset. Read the accumulator BEFORE
5368 // `remove_dead_stream` evicts it below.
5369 match reason {
5370 "H2::WindowStall" => {
5371 self.metric_events
5372 .push(MetricEvent::StreamReapedWindowStall);
5373 if matches!(self.stream_table.fc_stall_progress(sid), Some(acc) if acc > 0) {
5374 self.metric_events
5375 .push(MetricEvent::StreamReapedStallBudget);
5376 }
5377 }
5378 "H2::IdleTimeout" => self
5379 .metric_events
5380 .push(MetricEvent::StreamReapedIdleTimeout),
5381 other => debug!("{} unexpected reap reason {}", log_context!(self), other),
5382 }
5383 // Route through the canonical chokepoint so dedupe (rst_sent), the
5384 // pending-queue bound, and edge-triggered-epoll arming
5385 // (Readiness::arm_writable) all stay consistent — see LIFECYCLE
5386 // §8.2. The previous direct push bypassed all three: a mass reap
5387 // could push past the queue bound silently, a double-cancel pass
5388 // would grow the pending queue instead of short-circuiting on the
5389 // existing rst_sent membership, and the hand-rolled
5390 // `interest.insert(WRITABLE) + signal_pending_write` pair skipped
5391 // invariant 15.
5392 //
5393 // The CANCEL is Sōzu's own decision (`RstOrigin::Local`), so it is
5394 // not charged to the peer's MadeYouReset count: an idle stream is
5395 // also what a slow backend, a long poll or a paused download looks
5396 // like, and an idle stream pins one MAX_CONCURRENT_STREAMS slot for
5397 // the idle timeout without amplifying anything.
5398 //
5399 // We deliberately ignore the `Option<MuxResult>` signal here —
5400 // `cancel_timed_out_streams` returns `()` and is called as
5401 // best-effort housekeeping during the read path. A local reset
5402 // never trips the flood detector, and a queue overflow is
5403 // escalated by the next `flush_pending_control_frames`.
5404 //
5405 // A backend stream whose HEADERS never left is forgotten, not
5406 // reset: a RST_STREAM on a stream the backend has never seen is a
5407 // connection error that takes down every stream of the shared
5408 // connection (sozu-proxy/sozu#1631, see the `Position::Client` arm
5409 // of `ConnectionH2::end_stream`).
5410 let never_opened = matches!(self.position, Position::Client(..))
5411 && self
5412 .stream_table
5413 .get(sid)
5414 .is_some_and(|gid| !context.streams[gid].front.consumed);
5415 let queued_before = self.control_tx.lifetime_queued();
5416 if !never_opened {
5417 let _ = self.enqueue_rst(sid, H2Error::Cancel, RstOrigin::Local);
5418 }
5419 debug_assert!(
5420 !never_opened || self.control_tx.lifetime_queued() == queued_before,
5421 "a backend stream reaped before its HEADERS left must not be reset"
5422 );
5423
5424 // Remove from streams map and recycle the context stream so the slot
5425 // no longer counts against MAX_CONCURRENT_STREAMS.
5426 // Compute totals per-stream before remove (matches RST_STREAM handler).
5427 let byte_totals = self.compute_stream_byte_totals(context);
5428 if let Some(global_stream_id) = self.stream_table.get(sid) {
5429 {
5430 let stream = &mut context.streams[global_stream_id];
5431 self.attribute_bytes_to_stream(&mut stream.metrics);
5432 }
5433 // Check if stream is linked to a backend — borrow must be scoped
5434 // so end_stream can take &mut context.
5435 let linked_token = context.streams[global_stream_id].linked_token();
5436 let (client_rtt, server_rtt) = self.snapshot_rtts(&*endpoint, linked_token);
5437 if let Some(token) = linked_token {
5438 endpoint.end_stream(token, global_stream_id, context);
5439 }
5440 let stream = &mut context.streams[global_stream_id];
5441 match &self.position {
5442 Position::Client(_, backend, BackendStatus::Connected) => {
5443 context.record_backend_delta(backend, BackendChange::StreamsEnded(1));
5444 }
5445 Position::Client(..) => {}
5446 Position::Server => {
5447 self.distribute_overhead(&mut stream.metrics, byte_totals);
5448 stream.metrics.backend_stop();
5449 let events = stream.generate_access_log(
5450 true,
5451 Some(reason),
5452 context.listener.clone(),
5453 client_rtt,
5454 server_rtt,
5455 );
5456 self.metric_events.extend(events.into_iter().flatten());
5457 stream.state = StreamState::Recycle;
5458 }
5459 }
5460 // Retire sid from streams/prioriser/stream_last_activity_at and
5461 // invalidate expect_write/expect_read if they reference this gid.
5462 self.remove_dead_stream(sid, global_stream_id);
5463 }
5464 }
5465 // Writable arming is already done by enqueue_rst -> arm_writable in
5466 // the loop above; the trailing pair was redundant after the chokepoint
5467 // routing landed.
5468 }
5469
5470 /// Queue a `RST_STREAM` frame for serialisation by
5471 /// [`Self::flush_pending_control_frames`] on the next writable tick.
5472 ///
5473 /// This is the canonical entry point for proxy-emitted stream resets:
5474 /// `DATA` on a closed stream, `MAX_CONCURRENT_STREAMS` refusal, and the
5475 /// per-stream error paths in [`Self::reset_stream`] all funnel through
5476 /// here. `origin` says whether the peer provoked the reset; see
5477 /// [`RstOrigin`]. Serialisation is independent of the owning `Stream` still
5478 /// existing in `self.streams`, which is what lets us emit even after a
5479 /// caller has already called [`Self::remove_dead_stream`].
5480 ///
5481 /// Delegates the queueing itself to [`h2_control_tx::H2ControlTx::enqueue_rst`],
5482 /// which owns the invariants (dedupe via `rst_sent`, the per-insert queue
5483 /// bound and its overflow flag, edge-triggered-epoll arm via
5484 /// [`Readiness::arm_writable`]) and covers them with unit tests that need no
5485 /// `ConnectionH2` fixture. What stays here is the accounting, because a
5486 /// MadeYouReset cap trip converts to a connection-wide GOAWAY only this
5487 /// type can return.
5488 ///
5489 /// Two of the call paths that reach here are never inspected by
5490 /// [`Self::check_invariants`], whose only production caller is
5491 /// [`Self::handle_frame`]:
5492 ///
5493 /// * `cancel_timed_out_streams`, which queues one RST per timed-out stream
5494 /// in a single sweep and, from `Mux::timeout` against a silent peer,
5495 /// runs when `handle_frame` does not run at all;
5496 /// * the DATA-on-closed-stream reset, which sits in
5497 /// [`Self::handle_header_state`] — `handle_read` returns into that helper
5498 /// directly and that branch never reaches `handle_frame`.
5499 ///
5500 /// The second path is separately rate-limited: it is preceded by
5501 /// `H2FloodDetector::record_glitch` + `check_flood_or_return!`, whose
5502 /// `DEFAULT_MAX_GLITCH_COUNT` (2000) bounds it inside one flood window, and
5503 /// the window can only half-decay when `Mux::ready_inner` resamples
5504 /// `context.now` on a new outer iteration — which is strictly after an
5505 /// inner iteration that already ran `writable()`, since `arm_writable`
5506 /// raises both the WRITABLE interest and its event bit. So it cannot
5507 /// accumulate across windows without a `flush_pending_control_frames`
5508 /// pass in between. The per-insert bound inside
5509 /// [`h2_control_tx::H2ControlTx::enqueue_rst`] is what makes that reasoning
5510 /// unnecessary for correctness.
5511 fn enqueue_rst(
5512 &mut self,
5513 wire_stream_id: StreamId,
5514 error: H2Error,
5515 origin: RstOrigin,
5516 ) -> Option<MuxResult> {
5517 // Only a registered stream's id goes into `rst_sent`: eviction from
5518 // the stream table is what removes it again. A refused stream or a
5519 // closed one was never (or is no longer) registered, and its id would
5520 // otherwise stay in the set for the connection's lifetime. A refusal
5521 // records its id in the bounded recently-reset set instead
5522 // (`H2StreamTable::remember_refused`).
5523 let registered = self.stream_table.get(wire_stream_id).is_some();
5524 let rst_sent = registered.then(|| self.stream_table.rst_sent_mut());
5525 let outcome =
5526 self.control_tx
5527 .enqueue_rst(rst_sent, &mut self.readiness, wire_stream_id, error);
5528 // Account ONLY when a new RST actually entered the queue.
5529 // Calling `enqueue_rst` for a stream that already has a queued
5530 // (or already-flushed) RST is the dedup short-circuit — counting
5531 // those would inflate `h2.frames.tx.rst_stream` /
5532 // `h2.rst_stream.sent.*` and the CVE-2025-8671 MadeYouReset
5533 // count on frames that never reached the wire. An
5534 // at-capacity refusal is accounted the same way, and for the same
5535 // reason: that frame never reaches the wire either.
5536 //
5537 // Account at queue-time, not at drain-time. Doing it later in
5538 // `flush_pending_control_frames` would double-count any RST that
5539 // a re-entrant call (DATA on a closed stream we already RSTed)
5540 // tried to enqueue — and missing it at queue-time leaves
5541 // `cancel_timed_out_streams` / `refuse_stream_and_discard` /
5542 // DATA-on-closed-stream paths bypassing the lifetime cap
5543 // (security review LISA-001 on commit `da845c71`).
5544 match outcome {
5545 h2_control_tx::EnqueueRstOutcome::Queued => self.account_emitted_rst(error, origin),
5546 h2_control_tx::EnqueueRstOutcome::Deduped => None,
5547 // Drop + metric + contextual log, never a panic on the release
5548 // path. No GOAWAY is raised here: the refusal set
5549 // `H2ControlTx::overflowed`, which
5550 // `flush_pending_control_frames` escalates to
5551 // `GOAWAY(ENHANCE_YOUR_CALM)` before its drain loop for as long as
5552 // the connection is not already in `H2State::GoAway`/`Error` — and
5553 // once it is, the peer already holds a GOAWAY and `writable()`
5554 // force-disconnects. See `EnqueueRstOutcome::Dropped`. Raising a
5555 // second one from here would re-enter `goaway()` once per
5556 // remaining reaped stream, clobbering `self.zero` and inflating
5557 // `h2.goaway.sent.*`.
5558 h2_control_tx::EnqueueRstOutcome::Dropped => {
5559 error!(
5560 "{} RST_STREAM dropped: pending queue already at capacity ({}), stream={} error={:?}",
5561 log_context!(self),
5562 self.control_tx.max_pending(),
5563 wire_stream_id,
5564 error
5565 );
5566 self.metric_events.push(MetricEvent::RstStreamDropped);
5567 None
5568 }
5569 }
5570 }
5571
5572 /// Single accounting site for proxy-emitted RST_STREAM frames.
5573 /// Every emitted RST bumps the global tx counter and the per-error
5574 /// breakdown; only a non-`NoError` reset the peer provoked
5575 /// ([`RstOrigin::PeerProvoked`]) also feeds the MadeYouReset emitted-RST
5576 /// cap.
5577 ///
5578 /// Two distinct emission paths feed this helper:
5579 /// * Queued frames — `Self::enqueue_rst` (and therefore every
5580 /// callable that funnels through it: `reset_stream`,
5581 /// `refuse_stream_and_discard`, `cancel_timed_out_streams`,
5582 /// DATA-on-closed-stream) calls this once at queue-time. The
5583 /// drain in `flush_pending_control_frames` does NOT call it
5584 /// again — that would double-count.
5585 /// * Converter-emitted frames — the converter's `initialize`
5586 /// chokepoint (and the HPACK over-budget abort path) writes
5587 /// RST_STREAM frames straight into `kawa.out` from inside
5588 /// `kawa.prepare`. We collect those `H2Error` codes during the
5589 /// `write_streams` loop and call this helper for each one
5590 /// after the loop. They are all [`RstOrigin::Local`]: what errored
5591 /// is the message coming from the other side of the proxy — a
5592 /// backend response on a frontend connection, a client request on a
5593 /// backend one — never a frame this connection's peer sent.
5594 ///
5595 /// Returning `Some(MuxResult)` means the caller MUST short-circuit
5596 /// with that result — the flood detector tripped its lifetime cap
5597 /// and converted to a connection-wide GOAWAY.
5598 fn account_emitted_rst(&mut self, error: H2Error, origin: RstOrigin) -> Option<MuxResult> {
5599 self.metric_events.push(MetricEvent::RstStreamSent(error));
5600 if origin == RstOrigin::PeerProvoked && !matches!(error, H2Error::NoError) {
5601 if let Some(violation) = self.flood_detector.record_rst_emitted() {
5602 return Some(self.handle_flood_violation(violation));
5603 }
5604 // A provoked reset answers a protocol error by the peer, so it is
5605 // also a glitch: the per-window rate limit the ratio cap above
5606 // lacks. None of the `PeerProvoked` call sites records a glitch
5607 // of its own, so this is the only charge.
5608 self.flood_detector.record_glitch();
5609 if let Some(violation) = self.flood_detector.check_flood(self.now) {
5610 return Some(self.handle_flood_violation(violation));
5611 }
5612 }
5613 None
5614 }
5615
5616 /// Refuse a newly-opened stream with RST_STREAM and discard its HEADERS payload.
5617 ///
5618 /// Used when MAX_CONCURRENT_STREAMS is exceeded or buffer pool is exhausted.
5619 /// Queues the RST_STREAM for the writable path (can't write to kawa.storage
5620 /// here because it is needed to discard the HEADERS payload).
5621 ///
5622 /// Also applies SETTINGS back-pressure per RFC 9113 §5.1.2: if refusals
5623 /// burst past [`BACKPRESSURE_REFUSAL_THRESHOLD`] within
5624 /// [`BACKPRESSURE_WINDOW_DURATION`], the advertised
5625 /// `SETTINGS_MAX_CONCURRENT_STREAMS` is halved via
5626 /// [`Self::apply_mcs_backpressure`].
5627 ///
5628 /// `discarded` is stashed in [`Self::discarded_field_block`] for the
5629 /// `H2State::Discard` arm of [`Self::handle_read`] to consume — see
5630 /// [`DiscardedFieldBlock`] for why the HPACK field block cannot simply be
5631 /// dropped with the rest of the payload.
5632 fn refuse_stream_and_discard(
5633 &mut self,
5634 stream_id: StreamId,
5635 error: H2Error,
5636 origin: RstOrigin,
5637 payload_len: u32,
5638 discarded: DiscardedFieldBlock,
5639 ) -> MuxResult {
5640 if let Some(result) = self.enqueue_rst(stream_id, error, origin) {
5641 return result;
5642 }
5643 // RFC 9113 §5.1: the frames the peer sent on this stream before it
5644 // read the refusal are ignored, as on any stream Sōzu reset.
5645 self.stream_table.remember_refused(stream_id);
5646 let result = self.discard_field_block(stream_id, payload_len, discarded);
5647 self.record_refusal_for_backpressure();
5648 result
5649 }
5650
5651 /// Extend [`Self::flood_refused_streams`] with `stream_id`, refused under
5652 /// flood pressure, or start a new run when a stream was accepted since
5653 /// the last refusal (the even watermark `last_stream_id` moved past the
5654 /// run's first id).
5655 fn note_flood_refused_stream(&mut self, stream_id: StreamId) {
5656 let run = match self.flood_refused_streams {
5657 Some((first, _)) if self.last_stream_id < first => (first, stream_id),
5658 _ => (stream_id, stream_id),
5659 };
5660 debug_assert!(
5661 run.0 <= run.1 && run.1 == stream_id,
5662 "the refused run ends at the stream just refused"
5663 );
5664 debug_assert!(
5665 self.last_stream_id < run.0,
5666 "no accepted stream lies inside the refused run"
5667 );
5668 self.flood_refused_streams = Some(run);
5669 }
5670
5671 /// Read the `payload_len` bytes of a HEADERS or CONTINUATION frame of
5672 /// `stream_id` that is dropped through `H2State::Discard`, which decodes
5673 /// its field block only to keep the HPACK dynamic table in sync (RFC
5674 /// 9113 §4.3), then resumes normal reading. Queues nothing. A frame
5675 /// without END_HEADERS leaves its fragment in
5676 /// [`Self::pending_discarded_block`], so the CONTINUATION frames that
5677 /// complete the block are discarded and decoded with it.
5678 fn discard_field_block(
5679 &mut self,
5680 stream_id: StreamId,
5681 payload_len: u32,
5682 discarded: DiscardedFieldBlock,
5683 ) -> MuxResult {
5684 // A new block's first frame counts toward the block's size like an
5685 // accepted one's (CVE-2024-27316), so its CONTINUATION frames are
5686 // held to the same `max_header_list_size`; the whole frame payload
5687 // stands in for the fragment, which it bounds.
5688 if matches!(discarded, DiscardedFieldBlock::New { .. }) {
5689 self.flood_detector.begin_header_block_if_new(payload_len);
5690 }
5691 self.state = H2State::Discard;
5692 self.stream_table
5693 .set_expect_read(Some((H2StreamId::Zero, payload_len as usize)));
5694 self.discarded_field_block = Some((stream_id, discarded));
5695 MuxResult::Continue
5696 }
5697
5698 /// Minimally process and drop the payload of the DATA or HEADERS frame
5699 /// whose `header` was just parsed, for a stream that is reset or being
5700 /// reset (RFC 9113 §5.1): a field block, with the CONTINUATION frames
5701 /// completing it, still updates the HPACK decoder, DATA still counts
5702 /// toward the connection flow-control window.
5703 fn discard_frame_payload(&mut self, header: &FrameHeader) -> MuxResult {
5704 debug_assert!(
5705 matches!(header.frame_type, FrameType::Data | FrameType::Headers),
5706 "only DATA and HEADERS payloads are discarded per stream"
5707 );
5708 if header.frame_type == FrameType::Headers {
5709 self.discard_field_block(
5710 header.stream_id,
5711 header.payload_len,
5712 DiscardedFieldBlock::New {
5713 flags: header.flags,
5714 },
5715 )
5716 } else {
5717 self.skip_orphaned_data_payload(header.payload_len as usize, header.payload_len);
5718 MuxResult::Continue
5719 }
5720 }
5721
5722 /// RFC 9113 §5.4.2: answer a stream error on the open stream `header`
5723 /// names with RST_STREAM(`error`), tear the stream down, and discard the
5724 /// frame's payload ([`Self::discard_frame_payload`]). Every other stream
5725 /// on the connection carries on. The RST_STREAM goes through
5726 /// [`Self::reset_stream`], so it feeds the emitted-RST flood accounting.
5727 fn reset_stream_and_discard_frame<E, L>(
5728 &mut self,
5729 header: &FrameHeader,
5730 global_stream_id: GlobalStreamId,
5731 error: H2Error,
5732 context: &mut Context<L>,
5733 endpoint: E,
5734 ) -> MuxResult
5735 where
5736 E: Endpoint,
5737 L: ListenerHandler + L7ListenerHandler,
5738 {
5739 let result =
5740 self.reset_stream(header.stream_id, global_stream_id, context, endpoint, error);
5741 self.remove_dead_stream(header.stream_id, global_stream_id);
5742 if !matches!(result, MuxResult::Continue) {
5743 return result;
5744 }
5745 self.discard_frame_payload(header)
5746 }
5747
5748 /// RFC 9113 §5.1.2 SETTINGS back-pressure bookkeeping.
5749 ///
5750 /// Increments the refusal counter for the current back-pressure window
5751 /// and, when the burst threshold is crossed, halves the advertised
5752 /// `SETTINGS_MAX_CONCURRENT_STREAMS`. Further halving attempts in the
5753 /// same connection are suppressed by [`Self::mcs_backpressure_applied`]
5754 /// so sustained abuse does not collapse the cap to zero — callers can
5755 /// still promote the situation to `EnhanceYourCalm` via the flood
5756 /// detector.
5757 fn record_refusal_for_backpressure(&mut self) {
5758 if self.now.saturating_duration_since(self.refuse_window_start)
5759 >= BACKPRESSURE_WINDOW_DURATION
5760 {
5761 self.refuse_count_window = 0;
5762 self.refuse_window_start = self.now;
5763 }
5764 self.refuse_count_window = self.refuse_count_window.saturating_add(1);
5765 if !self.mcs_backpressure_applied
5766 && self.refuse_count_window >= BACKPRESSURE_REFUSAL_THRESHOLD
5767 {
5768 self.apply_mcs_backpressure();
5769 }
5770 }
5771
5772 /// Halve the advertised `SETTINGS_MAX_CONCURRENT_STREAMS` and mark the
5773 /// back-pressure state as applied. The new value takes effect locally
5774 /// immediately — subsequent stream-open checks in `handle_header_state`
5775 /// compare `self.stream_table.streams().len()` against this reduced cap, so the peer
5776 /// starts receiving `REFUSED_STREAM` earlier. A full SETTINGS re-send on
5777 /// the wire is deferred until we have a mid-connection SETTINGS queue
5778 /// (the existing path in `handle_preface_state` only fires during the
5779 /// handshake); this is noted in the task log as a minimal first step.
5780 fn apply_mcs_backpressure(&mut self) {
5781 let previous = self.local_settings.settings_max_concurrent_streams;
5782 let reduced = (previous / 2).max(1);
5783 warn!(
5784 "{} H2 SETTINGS back-pressure: refusals={} in {}s — halving \
5785 SETTINGS_MAX_CONCURRENT_STREAMS {} -> {}",
5786 log_context!(self),
5787 self.refuse_count_window,
5788 BACKPRESSURE_WINDOW_DURATION.as_secs(),
5789 previous,
5790 reduced,
5791 );
5792 self.local_settings.settings_max_concurrent_streams = reduced;
5793 self.mcs_backpressure_applied = true;
5794 }
5795
5796 /// Log a flood violation with full session context and emit the GOAWAY.
5797 ///
5798 /// Centralises the "flood detected" reporting so every site that observes a
5799 /// `H2FloodViolation` gets the same session-scoped log line, matching the
5800 /// RUSTLS log-context convention. Also emits the per-kind statsd counter
5801 /// (`h2.flood.violation.<kind>`) so SOC dashboards can window the trip
5802 /// rate without parsing logs — every CVE-mitigation in the H2 family
5803 /// (Rapid Reset, MadeYouReset, CONTINUATION/PING/SETTINGS floods, header
5804 /// overflow, glitch) funnels through this site.
5805 pub fn handle_flood_violation(&mut self, violation: H2FloodViolation) -> MuxResult {
5806 self.metric_events.push(MetricEvent::FloodViolation {
5807 metric_key: violation.metric_key,
5808 });
5809 warn!(
5810 "{} H2 flood detected: {} count {} exceeds threshold {}",
5811 log_context!(self),
5812 violation.reason,
5813 violation.count,
5814 violation.threshold,
5815 );
5816 self.goaway(violation.error)
5817 }
5818}
5819
5820/// Whether an answered frontend stream counts toward the RST caps'
5821/// denominator (`H2FloodDetector`'s `streams_opened`,
5822/// `lib/src/protocol/mux/h2_flood_detector.rs`): it was routed to a backend.
5823/// A backend answered it, a backend was selected for it — including when
5824/// Sōzu then answered 502/503/504 because that backend refused the
5825/// connection, failed or timed out — or its cluster has backends and none
5826/// could be selected because all of them are failing (`backends_unavailable`).
5827/// So a backend outage does not turn the client's ordinary cancels into a
5828/// Rapid Reset verdict. A stream answered before any backend was selected
5829/// for another reason does not count, whatever its status: no route, a
5830/// redirect, 401, 421, 429, a refusal, a 503 for a cluster with no backend,
5831/// or a session or buffer limit hit before selection. The peer opens those
5832/// for free, and counting them would let filler streams dilute the ratio.
5833fn routed_to_a_backend(stream: &crate::protocol::mux::Stream) -> bool {
5834 stream.metrics.backend_headers_received.is_some()
5835 || stream.context.backend_id.is_some()
5836 || stream.context.backends_unavailable
5837}
5838
5839/// Who decided a proxy-emitted `RST_STREAM`, for flood accounting.
5840///
5841/// Only a reset the peer provoked counts toward the CVE-2025-8671
5842/// MadeYouReset cap (`H2FloodDetector::record_rst_emitted`,
5843/// `lib/src/protocol/mux/h2_flood_detector.rs`). A reset Sōzu decides on its
5844/// own — the idle reaper's `CANCEL`, a `REFUSED_STREAM` from its own
5845/// concurrency limit, back-pressure, buffer pool or flood soft state, `STREAM_CLOSED` for DATA
5846/// on a stream it already closed, the converter's error on a backend failure —
5847/// says nothing about the peer, and counting it would end healthy connections
5848/// for Sōzu's own decisions.
5849#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5850enum RstOrigin {
5851 /// A frame the peer sent forced the reset: Content-Length mismatch,
5852 /// header parse error, oversized header block, PRIORITY
5853 /// self-dependency, zero-increment or overflowing WINDOW_UPDATE.
5854 PeerProvoked,
5855 /// Sōzu reset the stream on its own initiative.
5856 Local,
5857}
5858
5859/// Recover the [`H2Error`] code that the converter's `initialize`
5860/// chokepoint will encode into the synthesised RST_STREAM frame for a
5861/// kawa stuck in [`kawa::ParsingPhase::Error`]. Mirrors the parse +
5862/// fallback at `lib/src/protocol/mux/converter.rs::initialize` so the
5863/// flood-accounting helper sees the same code that lands on the wire.
5864fn rst_error_from_kawa<T: kawa::AsBuffer>(kawa: &kawa::Kawa<T>) -> H2Error {
5865 match kawa.parsing_phase {
5866 kawa::ParsingPhase::Error {
5867 kind: kawa::ParsingErrorKind::Processing { message },
5868 ..
5869 } => message.parse::<H2Error>().unwrap_or(H2Error::InternalError),
5870 _ => H2Error::InternalError,
5871 }
5872}
5873
5874/// Compile-time mapping from `(prefix, H2Error)` to a static metric key.
5875///
5876/// Materialises a `&'static str` literal via `concat!`, so the metric key
5877/// never crosses through a heap allocation and the statsd drain can store it
5878/// as `&'static str`. Adding a new `H2Error` variant fails the build here —
5879/// the metric breakdown stays in lock-step with RFC 9113 §7 codes.
5880///
5881/// Used for the per-error-code counters emitted around GOAWAY and RST_STREAM
5882/// in either direction (see `metric_for_goaway_sent` etc. below).
5883macro_rules! h2_error_metric_key {
5884 ($prefix:literal, $error:expr) => {
5885 match $error {
5886 H2Error::NoError => concat!($prefix, ".no_error"),
5887 H2Error::ProtocolError => concat!($prefix, ".protocol_error"),
5888 H2Error::InternalError => concat!($prefix, ".internal_error"),
5889 H2Error::FlowControlError => concat!($prefix, ".flow_control_error"),
5890 H2Error::SettingsTimeout => concat!($prefix, ".settings_timeout"),
5891 H2Error::StreamClosed => concat!($prefix, ".stream_closed"),
5892 H2Error::FrameSizeError => concat!($prefix, ".frame_size_error"),
5893 H2Error::RefusedStream => concat!($prefix, ".refused_stream"),
5894 H2Error::Cancel => concat!($prefix, ".cancel"),
5895 H2Error::CompressionError => concat!($prefix, ".compression_error"),
5896 H2Error::ConnectError => concat!($prefix, ".connect_error"),
5897 H2Error::EnhanceYourCalm => concat!($prefix, ".enhance_your_calm"),
5898 H2Error::InadequateSecurity => concat!($prefix, ".inadequate_security"),
5899 H2Error::HTTP11Required => concat!($prefix, ".http_1_1_required"),
5900 }
5901 };
5902}
5903
5904/// Static metric key for an outbound GOAWAY. Same call shape as the other three
5905/// helpers below — keeps the call sites uniform.
5906fn metric_for_goaway_sent(error: H2Error) -> &'static str {
5907 h2_error_metric_key!("h2.goaway.sent", error)
5908}
5909
5910/// Static metric key for an inbound GOAWAY by raw wire error code. Codes
5911/// outside RFC 9113 §7 fall into the dedicated `…unknown_error` bucket so the
5912/// breakdown stays bounded and operators can still spot non-standard peers.
5913fn metric_for_goaway_received(error_code: u32) -> &'static str {
5914 H2Error::try_from(error_code)
5915 .map(|e| h2_error_metric_key!("h2.goaway.received", e))
5916 .unwrap_or("h2.goaway.received.unknown_error")
5917}
5918
5919/// Static metric key for an outbound RST_STREAM. Mirrors
5920/// [`metric_for_goaway_sent`] under a separate namespace so RST and GOAWAY
5921/// rates can be alerted on independently.
5922fn metric_for_rst_stream_sent(error: H2Error) -> &'static str {
5923 h2_error_metric_key!("h2.rst_stream.sent", error)
5924}
5925
5926/// Static metric key for an inbound RST_STREAM by raw wire error code. Same
5927/// `…unknown_error` fallback as [`metric_for_goaway_received`].
5928fn metric_for_rst_stream_received(error_code: u32) -> &'static str {
5929 H2Error::try_from(error_code)
5930 .map(|e| h2_error_metric_key!("h2.rst_stream.received", e))
5931 .unwrap_or("h2.rst_stream.received.unknown_error")
5932}
5933
5934/// Static metric key for an inbound H2 frame by RFC 9113 §6 frame type.
5935/// Emitted at the `handle_frame` dispatch — single chokepoint that any
5936/// new H2 frame type must traverse, so adding a `Frame::*` variant fails
5937/// the build here. Counts are per-frame, not per-byte; pair with
5938/// `bytes_in` for traffic-mix dashboards.
5939fn h2_frame_rx_metric_key(frame_type: FrameType) -> &'static str {
5940 match frame_type {
5941 FrameType::Data => "h2.frames.rx.data",
5942 FrameType::Headers => "h2.frames.rx.headers",
5943 FrameType::PushPromise => "h2.frames.rx.push_promise",
5944 FrameType::Priority => "h2.frames.rx.priority",
5945 FrameType::RstStream => "h2.frames.rx.rst_stream",
5946 FrameType::Settings => "h2.frames.rx.settings",
5947 FrameType::Ping => "h2.frames.rx.ping",
5948 FrameType::GoAway => "h2.frames.rx.goaway",
5949 FrameType::WindowUpdate => "h2.frames.rx.window_update",
5950 FrameType::Continuation => "h2.frames.rx.continuation",
5951 FrameType::PriorityUpdate => "h2.frames.rx.priority_update",
5952 FrameType::Unknown(_) => "h2.frames.rx.unknown",
5953 }
5954}
5955
5956impl ConnectionH2 {
5957 /// Queue the final GOAWAY with `last_stream_id` set to the highest
5958 /// peer-initiated stream observed (`H2StreamTable::highest_peer_stream_id`).
5959 pub fn goaway(&mut self, error: H2Error) -> MuxResult {
5960 let last_stream_id = self.stream_table.highest_peer_stream_id();
5961 self.goaway_with_last_stream_id(error, last_stream_id)
5962 }
5963
5964 /// True while a peer HEADERS block is still being reassembled
5965 /// (`H2State::ContinuationHeader` / `H2State::ContinuationFrame`).
5966 pub fn peer_header_block_in_progress(&self) -> bool {
5967 matches!(
5968 self.state,
5969 H2State::ContinuationHeader(_) | H2State::ContinuationFrame(_)
5970 )
5971 }
5972
5973 /// RFC 9113 §6.8: the final GOAWAY a draining connection sends when its
5974 /// graceful-shutdown budget elapses, just before `Mux::shutting_down_inner`
5975 /// (`lib/src/protocol/mux/mod.rs`) closes it. Without it a forced close
5976 /// leaves the peer holding only the advisory `2^31-1` GOAWAY, which says
5977 /// nothing about which streams were processed (sozu-proxy/sozu#1647).
5978 ///
5979 /// A stream whose opening field block is still being reassembled is
5980 /// excluded: it is `StreamState::Idle`, it never reached a backend, and
5981 /// the peer may retry it. `create_stream` already counted it in
5982 /// `H2StreamTable::highest_peer_stream_id` when its HEADERS frame header
5983 /// arrived. Every stream `create_stream` accepted before it has a lower
5984 /// id (RFC 9113 §5.1.1, enforced by `handle_header_state` against
5985 /// `Self::last_stream_id`), and none can be accepted after it while the
5986 /// block is open (§6.10), so every stream sozu may have acted on is below
5987 /// it: the reported id is the one two below it (the previous odd id, or
5988 /// 0). A higher id counted in `highest_peer_stream_id` can only be a
5989 /// refused stream, which sozu did not act on either.
5990 ///
5991 /// A connection already in `H2State::GoAway` or `H2State::Error` has
5992 /// sent (or failed to send) its final GOAWAY and gets no second one.
5993 pub fn goaway_before_forced_close<L>(&mut self, context: &Context<L>) -> MuxResult
5994 where
5995 L: ListenerHandler + L7ListenerHandler,
5996 {
5997 debug_assert!(
5998 self.drain.draining(),
5999 "a forced close follows a graceful drain, which marks the connection draining"
6000 );
6001 if matches!(self.state, H2State::GoAway | H2State::Error) {
6002 return MuxResult::Continue;
6003 }
6004 let highest = self.stream_table.highest_peer_stream_id();
6005 let last_stream_id = match &self.state {
6006 H2State::ContinuationHeader(headers) | H2State::ContinuationFrame(headers)
6007 if self
6008 .stream_table
6009 .get(headers.stream_id)
6010 .and_then(|global_stream_id| context.streams.get(global_stream_id))
6011 .is_some_and(|stream| stream.state == StreamState::Idle) =>
6012 {
6013 headers.stream_id.saturating_sub(2)
6014 }
6015 _ => highest,
6016 };
6017 debug_assert!(
6018 last_stream_id <= highest && (last_stream_id == 0 || last_stream_id & 1 == 1),
6019 "the final GOAWAY reports 0 or a peer-initiated id no higher than any observed"
6020 );
6021 self.goaway_with_last_stream_id(H2Error::NoError, last_stream_id)
6022 }
6023
6024 fn goaway_with_last_stream_id(
6025 &mut self,
6026 error: H2Error,
6027 last_stream_id: StreamId,
6028 ) -> MuxResult {
6029 self.state = H2State::Error;
6030 self.drain.enter_final_goaway();
6031 self.graceful_goaway = error == H2Error::NoError;
6032 self.stream_table.set_expect_read(None);
6033 // Disarm the SETTINGS ACK timer: once we've committed to GOAWAY, the
6034 // timeout check at `readable()` / `flush_pending_control_frames()` must
6035 // not re-fire. Without this, `signal_pending_write()` below re-enters
6036 // `writable()` → `flush_pending_control_frames()` on the next tick,
6037 // the elapsed check is still true, and we emit another
6038 // `warn!` + `goaway()` pair, each bumping `h2.goaway.sent.*`.
6039 self.settings_sent_at = None;
6040 // Severity tiering: only `InternalError` implies a sozu-side bug when
6041 // WE emit it. Every other non-`NoError` reason is "peer misbehaved,
6042 // sozu defended correctly" — operators don't need paging on abusive
6043 // or buggy peers. Caller sites already log the specific antecedent
6044 // (flood detected, parser failure, SETTINGS timeout, invalid window)
6045 // before reaching `goaway()`, so demoting this summary line avoids
6046 // duplicate noise without hiding the root cause.
6047 match error {
6048 H2Error::NoError => debug!("{} GOAWAY: {:?}", log_context!(self), error),
6049 H2Error::InternalError => error!("{} GOAWAY: {:?}", log_context!(self), error),
6050 _ => warn!("{} GOAWAY: {:?}", log_context!(self), error),
6051 }
6052 self.metric_events.push(MetricEvent::GoAwaySent(error));
6053
6054 // RFC 9113 §6.8: last_stream_id is the highest peer-initiated stream
6055 // we processed. Queued behind whatever output is already waiting (an
6056 // ACK, the rest of a stream frame), never in place of it.
6057 match self.output.push_frames(GOAWAY_FRAME_SIZE, |buf| {
6058 serializer::gen_goaway(buf, last_stream_id, error).map(|(_, size)| size)
6059 }) {
6060 Ok(_) => {
6061 self.metric_events.push(MetricEvent::GoAwayFrameSent);
6062 self.state = H2State::GoAway;
6063 self.readiness.interest = Ready::WRITABLE | Ready::HUP | Ready::ERROR;
6064 self.readiness.signal_pending_write();
6065 MuxResult::Continue
6066 }
6067 Err(error) => {
6068 error!(
6069 "{} Could not serialize GoAwayFrame: {:?}",
6070 log_context!(self),
6071 error
6072 );
6073 self.force_disconnect()
6074 }
6075 }
6076 }
6077
6078 /// RFC 9113 §6.8: Initiate graceful shutdown using the double-GOAWAY pattern.
6079 ///
6080 /// First call sends GOAWAY with `last_stream_id = 0x7FFFFFFF` (MAX) to signal
6081 /// the intent to stop accepting new streams while allowing in-flight streams
6082 /// to complete. The connection enters draining mode.
6083 ///
6084 /// When `draining` is already true (second invocation), sends the final GOAWAY
6085 /// with the actual `highest_peer_stream_id` so the peer knows which streams
6086 /// were processed.
6087 ///
6088 /// `now` is the caller's clock snapshot and is what arms the forced-close
6089 /// budget. It is a parameter rather than a read of `Self::now` because
6090 /// the caller that matters — `Mux::shutting_down` — runs outside
6091 /// `ready()` and therefore outside the pass that last refreshed the
6092 /// mirror. In-module callers pass `self.now`.
6093 pub fn graceful_goaway(&mut self, now: Instant) -> MuxResult {
6094 // The initial GOAWAY goes out even while a peer HEADERS/CONTINUATION
6095 // block is still being reassembled: RFC 9113 §4.3/§6.10 constrain the
6096 // block's sender, not a GOAWAY travelling the other way, and it is
6097 // queued in `self.output`, which shares nothing with `zero` or
6098 // `header_reassembly`. Deferring it (#1401, removed in #1637) let a
6099 // drain close a session whose only stream was mid-block without any
6100 // GOAWAY — see `h2_drain`'s module doc.
6101 match self.drain.begin_graceful_drain(now) {
6102 // Second GOAWAY: send with the real last_stream_id.
6103 GracefulDrainDecision::AlreadyDraining => self.goaway(H2Error::NoError),
6104 // First GOAWAY: advertise MAX stream ID so the peer knows we are
6105 // draining but does not yet know the cutoff. This gives in-flight
6106 // requests a chance to arrive before we commit to a final
6107 // last_stream_id.
6108 GracefulDrainDecision::SendInitial => self.send_initial_goaway(),
6109 }
6110 }
6111
6112 /// Serializes and queues the first, advisory GOAWAY
6113 /// (`NO_ERROR`, `last_stream_id = STREAM_ID_MAX`) of a graceful drain
6114 /// in the ordered output queue (`self.output`), behind whatever is
6115 /// already queued there.
6116 fn send_initial_goaway(&mut self) -> MuxResult {
6117 // Keep expect_read as-is: existing streams should continue reading
6118 // data during the drain window opened by the initial GOAWAY. Only
6119 // the final GOAWAY (via `goaway()`) removes READABLE.
6120 debug!(
6121 "{} GOAWAY (graceful, initial): last_stream_id=0x7FFFFFFF",
6122 log_context!(self)
6123 );
6124 // The initial GOAWAY sends NO_ERROR on the wire — count it under
6125 // the same per-code key as the final GOAWAY. The downstream alert
6126 // that wants to distinguish drain from termination compares
6127 // against the `h2.goaway.sent.no_error` rate (drain) vs the other
6128 // variants (termination on error).
6129 self.metric_events
6130 .push(MetricEvent::GoAwaySent(H2Error::NoError));
6131
6132 // Queued behind whatever output is already waiting, like the final one.
6133 match self.output.push_frames(GOAWAY_FRAME_SIZE, |buf| {
6134 serializer::gen_goaway(buf, STREAM_ID_MAX, H2Error::NoError).map(|(_, size)| size)
6135 }) {
6136 Ok(_) => {
6137 self.metric_events.push(MetricEvent::GoAwayFrameSent);
6138 // Stay in the current state so the connection can continue processing
6139 // existing streams. The final GOAWAY will transition to GoAway state.
6140 // Keep READABLE so in-flight request bodies can still be received
6141 // during the drain window. Only remove READABLE in the final GOAWAY
6142 // (via `goaway()`).
6143 self.readiness.arm_writable();
6144 MuxResult::Continue
6145 }
6146 Err(error) => {
6147 error!(
6148 "{} Could not serialize graceful GoAwayFrame: {:?}",
6149 log_context!(self),
6150 error
6151 );
6152 self.force_disconnect()
6153 }
6154 }
6155 }
6156
6157 /// Returns `true` when the graceful-shutdown budget armed by
6158 /// [`Self::graceful_goaway`] has elapsed. A return of `true` signals
6159 /// the enclosing session loop that the proxy-initiated drain must
6160 /// transition to a forced close: remaining streams will not complete
6161 /// in time and keeping the connection open past the deadline defeats
6162 /// the soft-stop SLA.
6163 ///
6164 /// Returns `false` when:
6165 /// - drain has not started yet (`started_at` is `None`),
6166 /// - the knob is `0` / `None` (indefinite wait explicitly opted in),
6167 /// - or the elapsed time is still within the configured budget.
6168 pub fn graceful_shutdown_deadline_elapsed(&self) -> bool {
6169 self.drain.deadline_elapsed(self.now)
6170 }
6171
6172 /// Arm the graceful-shutdown budget of a connection a proxy soft-stop
6173 /// finds already draining, which `graceful_goaway` is not called for: a
6174 /// peer GOAWAY drains without arming it. See
6175 /// `H2DrainState::arm_deadline_if_unarmed` (`h2_drain.rs`).
6176 pub fn arm_graceful_shutdown_deadline(&mut self, now: Instant) {
6177 self.drain.arm_deadline_if_unarmed(now);
6178 }
6179
6180 /// True when the reaper has queued control frames (`RST_STREAM`) into
6181 /// `h2_control_tx::H2ControlTx` that have not yet been serialized. Kept SEPARATE
6182 /// from [`H2Shell::has_pending_write`] because that probe gates connection close
6183 /// (the `mod.rs` close-gating sites) and must NOT treat a queued RST as a
6184 /// reason to keep the connection open; this probe is consulted ONLY by the
6185 /// `MuxState::timeout` flush gate to push a silent-peer `RST_STREAM(CANCEL)`
6186 /// onto the wire before the connection closes.
6187 /// Adopt the embedder's clock snapshot for this pass.
6188 ///
6189 /// `Self::now` is mirrored from `Context::now` at each public entry
6190 /// point rather than read from the host — invariant 20 — and a driver
6191 /// standing where `H2Shell::writable` stands has to perform that mirror
6192 /// itself. The other entry points that need it do it inside their own
6193 /// bodies; the write pass is assembled by its caller, so its mirror is
6194 /// exposed.
6195 pub fn adopt_now(&mut self, now: Instant) {
6196 self.now = now;
6197 }
6198
6199 /// The landing zone an [`H2ReadTarget::Fill`] names.
6200 ///
6201 /// The read half of the byte-in / byte-out surface: a driver answers
6202 /// `Fill` by filling this slice and reporting what it managed, and never
6203 /// needs to know which buffer it belongs to. `H2Shell::readable` fills it
6204 /// from a socket; `sim/tests/h2_simulation.rs` fills it from a
6205 /// `VecDeque<u8>`, and that is the whole difference between them.
6206 ///
6207 /// Takes `context` apart from `self` rather than borrowing all of `*self`,
6208 /// for the reason `read_space` gives: the caller must keep the rest of
6209 /// the connection borrowable while the slice is live.
6210 pub fn read_space<'a, L>(
6211 &'a mut self,
6212 context: &'a mut Context<L>,
6213 stream_id: H2StreamId,
6214 amount: usize,
6215 ) -> &'a mut [u8]
6216 where
6217 L: ListenerHandler + L7ListenerHandler,
6218 {
6219 read_space(
6220 &mut self.zero,
6221 &mut context.streams,
6222 &self.position,
6223 stream_id,
6224 amount,
6225 )
6226 }
6227
6228 /// Gather one [`H2WriteTarget::Transmit`]: the ordered output queue
6229 /// first, then `stream_id`'s queued frames, through
6230 /// [`h2_transmit::gather_after`]. Answers `(prefix_len, bytes_offered)`.
6231 ///
6232 /// # Safety
6233 ///
6234 /// [`h2_transmit::gather_after`]'s contract: neither this connection's
6235 /// output queue nor the stream's `kawa` may be mutated or dropped until
6236 /// `io_slices` is emptied, which [`h2_transmit::confirm`] does first.
6237 pub unsafe fn gather_transmit<L>(
6238 &self,
6239 context: &Context<L>,
6240 stream_id: H2StreamId,
6241 io_slices: &mut Vec<std::io::IoSlice<'static>>,
6242 ) -> (usize, usize)
6243 where
6244 L: ListenerHandler + L7ListenerHandler,
6245 {
6246 let H2StreamId::Other { gid, .. } = stream_id else {
6247 unreachable!("no transmit names the connection's input buffer");
6248 };
6249 let stream = &context.streams[gid];
6250 // The WRITE side of `Stream::split`: a server writes the response
6251 // from `back`, a client the request from `front`.
6252 let kawa = match self.position {
6253 Position::Client(..) => &stream.front,
6254 Position::Server => &stream.back,
6255 };
6256 // SAFETY: forwarded, see this function's contract.
6257 unsafe { h2_transmit::gather_after(self.output.pending(), kawa, io_slices) }
6258 }
6259
6260 /// The buffer an [`H2WriteTarget::Transmit`] names.
6261 ///
6262 /// The write half of the same surface, and the one a driver brackets with
6263 /// [`Self::gather_transmit`] and [`h2_transmit::confirm`] around its own
6264 /// vectored write. See `write_buffer` for why it takes `context` apart
6265 /// from `self`.
6266 pub fn write_buffer<'a, L>(
6267 &'a mut self,
6268 context: &'a mut Context<L>,
6269 stream_id: H2StreamId,
6270 ) -> &'a mut GenericHttpStream
6271 where
6272 L: ListenerHandler + L7ListenerHandler,
6273 {
6274 write_buffer(
6275 &mut self.zero,
6276 &mut context.streams,
6277 &self.position,
6278 stream_id,
6279 )
6280 }
6281
6282 /// The queued output bytes an [`H2ControlFlushTarget::FlushOutput`] is
6283 /// asking the caller to move, or empty when there are none left.
6284 pub fn output_pending(&self) -> &[u8] {
6285 self.output.pending()
6286 }
6287
6288 /// Whether [`Self::output_pending`] ends with the last bytes this
6289 /// connection will ever send, so the caller may queue the TLS
6290 /// `close_notify` behind them and flush both at once.
6291 ///
6292 /// Only [`Self::goaway`] sets [`H2State::GoAway`], and only once its final
6293 /// GOAWAY is queued; the state's writable arm then closes
6294 /// (`h2_close::goaway_close_action`). Every other condition rules out a
6295 /// write the same pass would still make after this flush:
6296 ///
6297 /// - no stream left: no stream frame follows, and no stream is parked
6298 /// (`H2StreamTable::remove` nulls its park);
6299 /// - no WINDOW_UPDATE or RST_STREAM queued: the preamble already moved
6300 /// every one of them into the output this flush drains;
6301 /// - `Position::Server`: `close_notify` is a frontend TLS alert, as in
6302 /// [`H2Shell::initiate_close_notify`];
6303 /// - `close_notify_sent` unset: the alert is queued once.
6304 fn output_flush_closes_connection(&self) -> bool {
6305 self.position.is_server()
6306 && matches!(self.state, H2State::GoAway)
6307 && !self.close_notify_sent
6308 && self.stream_table.streams().is_empty()
6309 && self.flow_control.pending_window_updates_is_empty()
6310 && !self.control_tx.has_pending()
6311 }
6312
6313 /// Account for one write of [`Self::output_pending`], and answer whether
6314 /// the caller should stop.
6315 ///
6316 /// `true` means the write stalled (WouldBlock or a zero-length write) and
6317 /// the caller must wait for its next writable event; `false` means carry
6318 /// on until [`Self::output_pending`] is empty.
6319 pub fn consume_output_flush(&mut self, size: usize, status: SocketResult) -> bool {
6320 self.note_write_status(status);
6321 self.consume_output(size);
6322 update_readiness_after_write(size, status, &mut self.readiness)
6323 }
6324
6325 /// [`H2Shell::has_pending_write`] with the TLS answer supplied instead of
6326 /// queried — the byte-in / byte-out spelling of the same probe.
6327 ///
6328 /// A driver with no TLS layer passes `false` and gets the connection's own
6329 /// two conditions back.
6330 pub fn has_pending_write_with(&self, tls_wants_write: bool) -> bool {
6331 if self.peer_gone_after_final_goaway() {
6332 return false;
6333 }
6334 self.stream_table.expect_write().is_some() || !self.output.is_empty() || tls_wants_write
6335 }
6336
6337 pub fn has_pending_control_write(&self) -> bool {
6338 self.control_tx.has_pending()
6339 }
6340
6341 /// Number of streams currently tracked in the wire map. `stream_table` is
6342 /// private to this module, so this is the sole accessor for `router.rs`'s
6343 /// H2-connection-reuse heuristic (picks the non-draining H2 backend
6344 /// connection with the fewest active streams).
6345 pub fn stream_count(&self) -> usize {
6346 self.stream_table.len()
6347 }
6348
6349 pub fn create_stream<L>(
6350 &mut self,
6351 stream_id: StreamId,
6352 context: &mut Context<L>,
6353 ) -> Option<GlobalStreamId>
6354 where
6355 L: ListenerHandler + L7ListenerHandler,
6356 {
6357 // RFC 9113 §6.8: reject new streams on a draining connection
6358 if self.drain.draining() {
6359 error!(
6360 "{} Rejecting new stream {} on draining connection",
6361 log_context!(self),
6362 stream_id
6363 );
6364 return None;
6365 }
6366 let streams_before = self.stream_table.len();
6367 // Track the highest peer-initiated stream ID for GoAway frames
6368 // before any early return, so GoAway always reports the correct last
6369 // stream. `observe_peer_stream_id` asserts the monotonic-non-decreasing
6370 // property itself.
6371 self.stream_table.observe_peer_stream_id(stream_id);
6372 // `Ulid::generate()` would read the wall clock and a thread-local RNG
6373 // from inside `rusty_ulid` — two host reaches no grep over this module
6374 // can see. `Context::next_request_id` composes the same ULID from the
6375 // two sources the embedder owns instead.
6376 let request_id = context.next_request_id();
6377 let global_stream_id =
6378 context.create_stream(request_id, self.peer_settings.settings_initial_window_size)?;
6379 self.last_stream_id = (stream_id + 2) & !1;
6380 self.stream_table
6381 .register(stream_id, global_stream_id, self.now);
6382 // Post-conditions: the stream is now reachable in both indices (see
6383 // `H2StreamTable::register`'s own postconditions), the active count
6384 // grew by exactly one (the id was not already present —
6385 // `handle_header_state` rejects re-used ids), and `last_stream_id` is
6386 // the even watermark just past this id so `new_stream_id` never collides.
6387 debug_assert_eq!(
6388 self.stream_table.len(),
6389 streams_before + 1,
6390 "create_stream must add exactly one stream (id must not pre-exist)"
6391 );
6392 debug_assert!(
6393 self.last_stream_id > stream_id && self.last_stream_id & 1 == 0,
6394 "last_stream_id watermark must be the even value strictly above stream_id"
6395 );
6396 Some(global_stream_id)
6397 }
6398
6399 pub fn new_stream_id(&mut self) -> Option<StreamId> {
6400 let watermark_before = self.last_stream_id;
6401 let (issued, next) = next_stream_id(self.last_stream_id, self.position.is_client())?;
6402 self.last_stream_id = next;
6403 // Post-conditions: the locally-issued id has the parity of our role and
6404 // the watermark advanced strictly (so the next allocation cannot reuse
6405 // this id). `next_stream_id` already asserts parity vs `is_client`; here
6406 // we re-assert against `self.position` and the watermark monotonicity.
6407 debug_assert_eq!(
6408 issued & 1 == 1,
6409 self.position.is_client(),
6410 "locally-issued stream id parity must match our role"
6411 );
6412 debug_assert!(
6413 self.last_stream_id > watermark_before,
6414 "issuing a stream id must advance the watermark"
6415 );
6416 Some(issued)
6417 }
6418
6419 /// Test-only setter: jump `last_stream_id` close to `STREAM_ID_MAX` so
6420 /// that the next call to [`Self::new_stream_id`] exhausts the 31-bit
6421 /// space. FIX-22 ("Stream-ID exhaustion disconnects backend gracefully")
6422 /// exercises the `None`-return branch — reaching it through normal API
6423 /// usage would require issuing ~2³¹ requests, which is not tractable in
6424 /// an E2E harness.
6425 #[cfg(any(test, feature = "e2e-hooks"))]
6426 pub fn __test_set_last_stream_id(&mut self, id: StreamId) {
6427 self.last_stream_id = id;
6428 }
6429
6430 /// Test-only setter: map `stream_id -> gid` on the wire WITHOUT arming
6431 /// the per-stream liveness timer `stream_table.register` otherwise always
6432 /// pairs it with. `mod::tests::shutting_down_refreshes_the_snapshot_so_the_drain_budget_expires`
6433 /// needs exactly this artificial state, to force `cancel_timed_out_streams`
6434 /// down its early-return path (empty activity map) rather than race an
6435 /// RST_STREAM into that test's unrelated drain-budget assertion.
6436 #[cfg(any(test, feature = "e2e-hooks"))]
6437 pub fn __test_insert_wire_mapping_only(&mut self, stream_id: StreamId, gid: GlobalStreamId) {
6438 self.stream_table
6439 .__test_insert_wire_mapping_only(stream_id, gid);
6440 }
6441
6442 /// Cross-field invariant sweep for the H2 connection state machine,
6443 /// asserted as a run-to-completion post-condition at the end of every
6444 /// frame-handling pass (see the call in [`Self::handle_frame`]).
6445 ///
6446 /// These are relationships between *separate* fields that no single setter
6447 /// can guarantee on its own — exactly the class of bug TigerStyle's
6448 /// `check_invariants` targets. Each one is cheap (counter compares + a few
6449 /// `HashMap` membership probes); the whole function is `#[cfg(debug_assertions)]`
6450 /// and compiles out of release entirely.
6451 ///
6452 /// Encoded invariants:
6453 /// 1. **Stream-id watermark parity**: locally-issued ids never exceed
6454 /// `STREAM_ID_MAX`; `last_stream_id` stays the even watermark (it is
6455 /// rounded to `(id + 2) & !1` and initialised to 0).
6456 /// 2. **Per-stream caches are subsets of the live stream set**:
6457 /// `stream_last_activity_at` is keyed only by currently-tracked stream
6458 /// ids — a leak here would let a removed stream keep an idle timer and
6459 /// mis-fire `cancel_timed_out_streams`. (`rst_sent` is intentionally NOT
6460 /// a subset: a queued RST for an already-removed stream is legal.)
6461 /// 3. **RST queue accounting** is checked by
6462 /// [`h2_control_tx::H2ControlTx::check_invariants`] as a post-condition
6463 /// of its own mutating methods, so it is not restated here — a second
6464 /// copy of a rule drifts from the first.
6465 /// 4. **Pending WINDOW_UPDATE bound**: the coalescing map never exceeds the
6466 /// per-connection cap derived from `max_concurrent_streams`.
6467 /// 5. **Drain/state coupling**: a terminal `GoAway`/`Error` state implies the
6468 /// connection is draining (`goaway()` sets both); the converse need not
6469 /// hold (graceful drain stays in a live state).
6470 #[cfg(debug_assertions)]
6471 fn check_invariants<L>(&self, context: &Context<L>)
6472 where
6473 L: ListenerHandler + L7ListenerHandler,
6474 {
6475 // (1) Watermark parity and bound.
6476 debug_assert!(
6477 self.last_stream_id & 1 == 0,
6478 "last_stream_id must stay an even watermark, got {}",
6479 self.last_stream_id
6480 );
6481
6482 // (2) Per-stream caches are subsets of the live stream set, and every
6483 // mapping points at a valid context slot.
6484 debug_assert!(
6485 self.stream_table
6486 .stream_last_activity_at()
6487 .keys()
6488 .all(|id| self.stream_table.streams().contains_key(id)),
6489 "stream_last_activity_at must only track currently-open stream ids"
6490 );
6491 debug_assert!(
6492 self.stream_table
6493 .streams()
6494 .values()
6495 .all(|&gid| gid < context.streams.len()),
6496 "every stream mapping must point at a valid context slot"
6497 );
6498
6499 // (4) Pending WINDOW_UPDATE coalescing map bound.
6500 debug_assert!(
6501 self.flow_control.pending_window_updates_len() <= self.max_pending_window_updates,
6502 "pending WINDOW_UPDATE map must stay within its per-connection cap"
6503 );
6504
6505 // (5) Drain/state coupling: terminal states imply draining.
6506 debug_assert!(
6507 !matches!(self.state, H2State::GoAway | H2State::Error) || self.drain.draining(),
6508 "GoAway/Error state must imply the connection is draining"
6509 );
6510 }
6511
6512 fn handle_frame<E, L>(
6513 &mut self,
6514 frame: Frame,
6515 wire_payload_len: u32,
6516 context: &mut Context<L>,
6517 endpoint: E,
6518 ) -> MuxResult
6519 where
6520 E: Endpoint,
6521 L: ListenerHandler + L7ListenerHandler,
6522 {
6523 trace!("{} {:#?}", log_context!(self), frame);
6524 // Per-frame-type RX counter. Single chokepoint covers every H2 frame
6525 // type — adding a new `Frame::*` variant fails the build inside the
6526 // helper, keeping the metric breakdown in lock-step with RFC 9113 §6.
6527 self.metric_events
6528 .push(MetricEvent::FrameReceived(frame.frame_type()));
6529 let result = match frame {
6530 Frame::Data(data) => self.handle_data_frame(data, wire_payload_len, context, endpoint),
6531 Frame::Headers(headers) => self.handle_headers_frame(headers, context, endpoint),
6532 Frame::PushPromise(_) => self.handle_push_promise_frame(),
6533 Frame::Priority(priority) => self.handle_priority_frame(priority, context, endpoint),
6534 Frame::RstStream(rst_stream) => {
6535 self.handle_rst_stream_frame(rst_stream, context, endpoint)
6536 }
6537 Frame::Settings(settings) => self.handle_settings_frame(settings, context),
6538 Frame::Ping(ping) => self.handle_ping_frame(ping),
6539 Frame::GoAway(goaway) => self.handle_goaway_frame(goaway, context, endpoint),
6540 Frame::WindowUpdate(wu) => self.handle_window_update_frame(wu, context, endpoint),
6541 Frame::PriorityUpdate(pu) => self.handle_priority_update_frame(pu),
6542 Frame::Continuation(_) => {
6543 // Unreachable: standalone CONTINUATION is rejected in
6544 // `handle_header_state` (RFC 9113 §6.10) and in-block
6545 // CONTINUATION is consumed by the inline header-parsing
6546 // path. Keep a defensive fallback that returns
6547 // PROTOCOL_ERROR rather than panicking in debug builds.
6548 self.attribute_bytes_to_overhead();
6549 warn!(
6550 "{} CONTINUATION frames are handled inline during header parsing",
6551 log_context!(self)
6552 );
6553 self.goaway(H2Error::ProtocolError)
6554 }
6555 // RFC 9113 §5.5: unknown frame types MUST be ignored and discarded.
6556 // The parser already consumed the payload; attribute the bytes
6557 // to connection-level overhead and continue.
6558 Frame::Unknown(raw) => {
6559 debug!(
6560 "{} Ignoring unknown H2 frame type {}",
6561 log_context!(self),
6562 raw
6563 );
6564 self.attribute_bytes_to_overhead();
6565 MuxResult::Continue
6566 }
6567 };
6568 // Run-to-completion post-condition: the connection-level cross-field
6569 // invariants must hold after every frame is dispatched, on success and
6570 // on the protocol-error paths alike.
6571 #[cfg(debug_assertions)]
6572 self.check_invariants(context);
6573 result
6574 }
6575
6576 /// RFC 9110 §8.6: Content-Length validation must be skipped for responses
6577 /// where the body is absent by definition:
6578 /// - Responses to HEAD requests (any status)
6579 /// - 1xx informational responses
6580 /// - 204 No Content
6581 /// - 304 Not Modified
6582 fn content_length_exempt(
6583 &self,
6584 context: &crate::protocol::kawa_h1::editor::HttpContext,
6585 ) -> bool {
6586 use crate::protocol::kawa_h1::parser::Method;
6587 // HEAD method responses (only relevant when reading backend responses)
6588 if self.position.is_client() && context.method == Some(Method::Head) {
6589 return true;
6590 }
6591 // 1xx, 204, 304 status codes
6592 if let Some(status) = context.status
6593 && ((100..200).contains(&status) || status == 204 || status == 304)
6594 {
6595 return true;
6596 }
6597 false
6598 }
6599
6600 /// What `handle_data_frame` does with the DATA payload of a backend
6601 /// response that has no content by definition (RFC 9110 §6.4.1). Only
6602 /// meaningful when reading a backend response (`Position::Client`): on
6603 /// the server side `context.status` describes the response, not the
6604 /// request being read.
6605 fn backend_response_content(
6606 context: &crate::protocol::kawa_h1::editor::HttpContext,
6607 ) -> BackendResponseContent {
6608 use crate::protocol::kawa_h1::parser::Method;
6609 if matches!(context.status, Some(204 | 304)) {
6610 BackendResponseContent::Forbidden
6611 } else if context.method == Some(Method::Head) {
6612 BackendResponseContent::Discarded
6613 } else {
6614 BackendResponseContent::Forwarded
6615 }
6616 }
6617
6618 fn handle_data_frame<E, L>(
6619 &mut self,
6620 data: parser::Data,
6621 wire_payload_len: u32,
6622 context: &mut Context<L>,
6623 mut endpoint: E,
6624 ) -> MuxResult
6625 where
6626 E: Endpoint,
6627 L: ListenerHandler + L7ListenerHandler,
6628 {
6629 // CVE-2019-9518: track empty DATA frames (no payload, no END_STREAM)
6630 if data.payload.is_empty() && !data.end_stream {
6631 self.flood_detector.record_empty_data_frame();
6632 check_flood_or_return!(self);
6633 }
6634 let Some(global_stream_id) = self.stream_table.get(data.stream_id) else {
6635 // The stream was terminated while data was expected,
6636 // probably due to automatic answer for invalid/unauthorized access.
6637 // RFC 9113 §6.9: we MUST still account for the DATA payload in
6638 // connection-level flow control using the full wire length
6639 // (including pad-length byte and padding), otherwise the window
6640 // shrinks permanently and eventually stalls the connection.
6641 let conn_threshold = self.connection_config.initial_connection_window / 2;
6642 if let Some(increment) = self
6643 .flow_control
6644 .account_received_bytes(wire_payload_len, conn_threshold)
6645 {
6646 self.queue_window_update(0, increment);
6647 self.readiness.arm_writable();
6648 }
6649 self.attribute_bytes_to_overhead();
6650 return MuxResult::Continue;
6651 };
6652 let mut slice = data.payload;
6653 let stream = &mut context.streams[global_stream_id];
6654 // Unpadded application payload size — what is forwarded to the backend
6655 // and counted against Content-Length.
6656 let content_len = slice.len();
6657 // Full wire-payload size (includes pad-length byte and padding).
6658 // RFC 9113 §5.2: padding counts against flow-control windows.
6659 let wire_len = wire_payload_len as usize;
6660 let cl_exempt = self.content_length_exempt(&stream.context);
6661 let content = if self.position.is_client() {
6662 Self::backend_response_content(&stream.context)
6663 } else {
6664 BackendResponseContent::Forwarded
6665 };
6666
6667 // Extract declared content-length and update position-aware data counter
6668 let (data_received, declared_length) = {
6669 let parts = stream.split(&self.position);
6670 *parts.data_received += content_len;
6671 let total = *parts.data_received;
6672 let declared = match parts.rbuffer.body_size {
6673 kawa::BodySize::Length(n) => Some(n),
6674 _ => None,
6675 };
6676 (total, declared)
6677 };
6678
6679 // RFC 9113 §6.9 + §5.2: credit connection-level flow control BEFORE any
6680 // early-return path. Malformed DATA still consumed the peer's send
6681 // window; without crediting it back, repeated bad streams permanently
6682 // shrink the connection window and stall unrelated streams that share
6683 // the same H2 connection. Stream-level credit can stay below — once we
6684 // RST the violating stream, its per-stream window is moot per
6685 // RFC 9113 §6.9 (the receiver discards further frames for the stream).
6686 let conn_threshold = self.connection_config.initial_connection_window / 2;
6687 if let Some(increment) = self
6688 .flow_control
6689 .account_received_bytes(wire_payload_len, conn_threshold)
6690 {
6691 self.queue_window_update(0, increment);
6692 }
6693
6694 // RFC 9110 §15.3.5, §15.4.5, RFC 9113 §8.1.1: a 204 or a 304 cannot
6695 // contain content, so DATA carrying a payload makes it malformed:
6696 // reset the backend stream before the payload is queued, so it never
6697 // reaches the client (an H1 client would read it as the start of the
6698 // next response). `content_length_exempt` skips the length checks
6699 // below for these responses, so they would not catch it. A response
6700 // to HEAD only SHOULD NOT carry content (RFC 9110 §9.3.2): its payload
6701 // is discarded below instead.
6702 if content == BackendResponseContent::Forbidden && content_len > 0 {
6703 error!(
6704 "{} DATA with a {}-byte payload on a {:?} response (RFC 9110 §6.4.1)",
6705 log_context!(self),
6706 content_len,
6707 stream.context.status
6708 );
6709 incr!(names::h2::BODILESS_RESPONSE_DATA_RESET);
6710 if !self.flow_control.pending_window_updates_is_empty() {
6711 self.readiness.arm_writable();
6712 }
6713 let result = self.reset_stream(
6714 data.stream_id,
6715 global_stream_id,
6716 context,
6717 endpoint,
6718 H2Error::ProtocolError,
6719 );
6720 self.remove_dead_stream(data.stream_id, global_stream_id);
6721 return result;
6722 }
6723
6724 // RFC 9113 §8.1.1: if Content-Length is present, total DATA payload
6725 // must not exceed the declared length (check on every frame).
6726 // RFC 9110 §8.6: skip for HEAD/1xx/204/304 responses (body absent by definition).
6727 if !cl_exempt
6728 && let Some(expected) = declared_length
6729 && data_received > expected
6730 {
6731 error!(
6732 "{} Content-Length mismatch: received {} > declared {}",
6733 log_context!(self),
6734 data_received,
6735 expected
6736 );
6737 // Pair WRITABLE arming with the queued connection-level
6738 // WINDOW_UPDATE before returning; otherwise the credit sits
6739 // until the next inbound frame on this connection.
6740 if !self.flow_control.pending_window_updates_is_empty() {
6741 self.readiness.arm_writable();
6742 }
6743 let result = self.reset_stream(
6744 data.stream_id,
6745 global_stream_id,
6746 context,
6747 endpoint,
6748 H2Error::ProtocolError,
6749 );
6750 self.remove_dead_stream(data.stream_id, global_stream_id);
6751 return result;
6752 }
6753
6754 let stream = &mut context.streams[global_stream_id];
6755 self.attribute_bytes_to_stream(&mut stream.metrics);
6756 let stream_state = stream.state;
6757 let is_unlinked = matches!(stream_state, StreamState::Unlinked);
6758 let parts = stream.split(&self.position);
6759 let kawa = parts.rbuffer;
6760 self.position.count_bytes_in(parts.metrics, content_len);
6761
6762 // Stream-level flow control (only if stream is still open).
6763 // Connection-level credit was already applied above the CL check so
6764 // malformed DATA cannot starve the connection window for other streams.
6765 if !data.end_stream {
6766 self.queue_window_update(data.stream_id, wire_payload_len);
6767 }
6768
6769 // If we have pending updates, ensure we get a writable event.
6770 // Must use signal_pending_write() — not just interest.insert() — because
6771 // under edge-triggered epoll the WRITABLE event bit may have been consumed
6772 // by a previous write cycle. Without the event bit set, filter_interest()
6773 // returns 0 and the WINDOW_UPDATEs never get flushed, stalling the client.
6774 if !self.flow_control.pending_window_updates_is_empty() {
6775 self.readiness.arm_writable();
6776 }
6777
6778 // Refresh per-stream idle timer on non-empty DATA.
6779 // Empty DATA frames (CVE-2019-9518 vector) must NOT reset the timer,
6780 // otherwise an attacker can keep a stream alive indefinitely with
6781 // zero-length frames while pinning a MAX_CONCURRENT_STREAMS slot.
6782 if content_len > 0 {
6783 self.stream_table.touch_activity(data.stream_id, self.now);
6784 }
6785
6786 if is_unlinked {
6787 // Backend is gone but client is still sending DATA.
6788 // Discard the data (flow control updates were already
6789 // queued above) to prevent the buffer from filling up.
6790 kawa.storage.clear();
6791 if data.end_stream {
6792 kawa.parsing_phase = kawa::ParsingPhase::Terminated;
6793 self.mark_end_of_stream(stream);
6794 }
6795 } else {
6796 // A response to HEAD has no content (RFC 9110 §9.3.2): its
6797 // payload is never queued, as kawa's H1 parser leaves the bytes
6798 // an H1 backend sends after such a head unread, and its windows
6799 // were credited above.
6800 let discarded = content == BackendResponseContent::Discarded && content_len > 0;
6801 if discarded {
6802 // The frame's wire payload is the tail of the buffer: drop
6803 // it there. Kept behind `storage.head`, it would hold space
6804 // only the frontend consuming a block frees, and no block
6805 // refers to it, so a payload larger than the buffer would
6806 // stop the backend connection from being read before its
6807 // END_STREAM.
6808 debug_assert_eq!(
6809 kawa.storage.end,
6810 kawa.storage.head + wire_len,
6811 "a DATA payload being read is the tail of the stream buffer"
6812 );
6813 kawa.storage.end = kawa.storage.head;
6814 } else {
6815 // Advance storage.head by the full wire payload length so the
6816 // next frame doesn't read stale pad-length+padding bytes.
6817 slice.start = slice.start.saturating_add(kawa.storage.head as u32);
6818 kawa.storage.head += wire_len;
6819 }
6820 // Emit chunk framing for chunked transfer encoding (H2→H1 path).
6821 // H2 converter ignores ChunkHeader and end_chunk Flags, so this is safe for H2→H2.
6822 if kawa.body_size == kawa::BodySize::Chunked && content_len > 0 && !discarded {
6823 let hex_len = {
6824 let mut buf = Vec::with_capacity(16);
6825 let _ = write!(buf, "{content_len:x}");
6826 buf
6827 };
6828 kawa.push_block(kawa::Block::ChunkHeader(kawa::ChunkHeader {
6829 length: kawa::Store::from_vec(hex_len),
6830 }));
6831 }
6832
6833 if discarded {
6834 debug!(
6835 "{} DATA payload of {} bytes on a response to HEAD discarded",
6836 log_context!(self),
6837 content_len
6838 );
6839 } else {
6840 kawa.push_block(kawa::Block::Chunk(kawa::Chunk {
6841 data: kawa::Store::Slice(slice),
6842 }));
6843 }
6844
6845 if kawa.body_size == kawa::BodySize::Chunked && content_len > 0 && !discarded {
6846 kawa.push_block(kawa::Block::Flags(kawa::Flags {
6847 end_body: false,
6848 end_chunk: true,
6849 end_header: false,
6850 end_stream: false,
6851 }));
6852 }
6853
6854 if data.end_stream {
6855 // RFC 9113 §8.1.1: on end_stream, total DATA must equal Content-Length.
6856 // RFC 9110 §8.6: skip for HEAD/1xx/204/304 responses.
6857 if !cl_exempt
6858 && let Some(expected) = declared_length
6859 && data_received != expected
6860 {
6861 error!(
6862 "{} Content-Length mismatch: received {} != declared {}",
6863 log_context!(self),
6864 data_received,
6865 expected
6866 );
6867 let result = self.reset_stream(
6868 data.stream_id,
6869 global_stream_id,
6870 context,
6871 endpoint,
6872 H2Error::ProtocolError,
6873 );
6874 self.remove_dead_stream(data.stream_id, global_stream_id);
6875 return result;
6876 }
6877 let is_chunked = kawa.body_size == kawa::BodySize::Chunked;
6878 kawa.push_block(kawa::Block::Flags(kawa::Flags {
6879 end_body: true,
6880 end_chunk: is_chunked,
6881 end_header: false,
6882 end_stream: true,
6883 }));
6884 kawa.parsing_phase = kawa::ParsingPhase::Terminated;
6885 self.mark_end_of_stream(stream);
6886 }
6887 if let StreamState::Linked(token) = stream_state {
6888 // Mirror of `ConnectionH1::readable`'s close-delimited EOF branch
6889 // for the H2-backend → H2-frontend path: edge-triggered epoll will
6890 // NOT re-fire for bytes we just pushed into stream.back; the
6891 // synthetic event is the only wake path. LIFECYCLE invariant 15.
6892 endpoint.readiness_mut(token).arm_writable();
6893 self.metric_events
6894 .push(MetricEvent::WritableRearmedPeerData);
6895 }
6896 }
6897 MuxResult::Continue
6898 }
6899
6900 fn handle_headers_frame<E, L>(
6901 &mut self,
6902 headers: Headers,
6903 context: &mut Context<L>,
6904 mut endpoint: E,
6905 ) -> MuxResult
6906 where
6907 E: Endpoint,
6908 L: ListenerHandler + L7ListenerHandler,
6909 {
6910 // HEADERS frames represent real application activity (new request
6911 // or response). Reset the timeout since the peer is actively
6912 // communicating, unlike control frames (PING, WINDOW_UPDATE).
6913 self.arm_timeout();
6914 if !headers.end_headers {
6915 // CVE-2024-27316: only initialize tracking on the very first HEADERS
6916 // fragment, not on re-entries from ContinuationFrame (which call
6917 // handle_frame(Frame::Headers) with the accumulated header block).
6918 self.flood_detector
6919 .begin_header_block_if_new(headers.header_block_fragment.len);
6920 if !self.header_reassembly.is_in_progress() {
6921 // First HEADERS frame of this block: copy its fragment out of
6922 // `zero.storage` now, before the caller's `expect_header()`
6923 // starts overwriting it with the next frame's bytes. A
6924 // re-entry from `(H2State::ContinuationFrame(headers), _)`
6925 // already appended THIS frame's own payload to the
6926 // accumulator before calling back in here (see that match
6927 // arm in `handle_read()`), so there is nothing left to copy.
6928 //
6929 // `data_opt` (bounds-checked), not `data` (panics on OOB):
6930 // `header_block_fragment` is network-facing-derived — the
6931 // parser computes it correctly by construction, so this
6932 // should never actually be out of bounds, but "no panic on
6933 // network-facing input" (CLAUDE.md) applies regardless, and
6934 // the CVE-2024-27316 abort path below this one used to make
6935 // exactly this check before this step replaced its own
6936 // buffer access with `HeaderBlockAccumulator::finish()`
6937 // (owned bytes, no slicing left to bound-check there).
6938 let Some(fragment) = headers
6939 .header_block_fragment
6940 .data_opt(self.zero.storage.buffer())
6941 else {
6942 error!(
6943 "{} header_block_fragment out of bounds of zero.storage",
6944 log_context!(self)
6945 );
6946 return self.goaway(H2Error::InternalError);
6947 };
6948 self.header_reassembly.begin(fragment);
6949 self.zero.storage.clear();
6950 }
6951 debug!(
6952 "{} FRAGMENT: stream_id={}, accumulated_len={}",
6953 log_context!(self),
6954 headers.stream_id,
6955 self.header_reassembly.len()
6956 );
6957 self.state = H2State::ContinuationHeader(headers);
6958 return MuxResult::Continue;
6959 }
6960 // Header block is complete — reset CONTINUATION counters
6961 self.flood_detector.reset_continuation();
6962 // can this fail?
6963 let stream_id = headers.stream_id;
6964 let Some(global_stream_id) = self.stream_table.get(stream_id) else {
6965 error!(
6966 "{} Handling Headers frame with no attached stream {:#?}",
6967 log_context!(self),
6968 self
6969 );
6970 self.metric_events.push(MetricEvent::HeadersWithoutStream);
6971 self.attribute_bytes_to_overhead();
6972 return self.force_disconnect();
6973 };
6974
6975 // Refresh per-stream idle timer on HEADERS (response headers or trailers
6976 // on an existing stream). Initial HEADERS that create the stream already
6977 // set the timestamp in create_stream().
6978 self.stream_table.touch_activity(stream_id, self.now);
6979
6980 if let Some(priority) = &headers.priority
6981 && self.scheduler.push_priority(stream_id, priority.clone())
6982 {
6983 // This HEADERS frame's own block just completed (single-frame,
6984 // or the final CONTINUATION of a multi-frame sequence) — but
6985 // we're aborting before ever reaching the `finish()` call below.
6986 // If a CONTINUATION sequence fed it, `header_reassembly` is
6987 // still `is_in_progress() == true`; retire it here or it leaks
6988 // that flag into the NEXT HEADERS frame processed on this
6989 // connection, which may belong to a completely different
6990 // stream — that frame's own "read from zero.storage" fast path
6991 // would then be skipped in favour of these stale, already-
6992 // discarded bytes (sozu-proxy/sozu review of e1c3c2fb, B1).
6993 // Regression:
6994 // `a_priority_self_dependency_reset_does_not_leak_the_reassembly_accumulator`.
6995 if self.header_reassembly.is_in_progress() {
6996 self.header_reassembly.finish();
6997 }
6998 self.reset_stream(
6999 stream_id,
7000 global_stream_id,
7001 context,
7002 endpoint,
7003 H2Error::ProtocolError,
7004 );
7005 self.remove_dead_stream(stream_id, global_stream_id);
7006 return MuxResult::Continue;
7007 }
7008
7009 let stream = &mut context.streams[global_stream_id];
7010 self.attribute_bytes_to_stream(&mut stream.metrics);
7011 // The field-block bytes live in one of two places depending on how
7012 // we got here: the owned reassembly accumulator when a CONTINUATION
7013 // sequence fed this HEADERS (`header_reassembly.is_in_progress()`),
7014 // or straight out of `zero.storage` for the single-frame fast path
7015 // — the common case, which never touches the accumulator at all.
7016 // See `h2_header_reassembly.rs`.
7017 let buffer: &[u8] = if self.header_reassembly.is_in_progress() {
7018 self.header_reassembly.data()
7019 } else {
7020 // Invariant: only reachable when nothing is accumulating for
7021 // THIS stream's own block. Every early-return path above this
7022 // point that could have left a CONTINUATION-fed reassembly
7023 // `is_in_progress()` now retires it first (see the priority
7024 // self-dependency branch above — B1 in the review of e1c3c2fb
7025 // was exactly a return that skipped this and leaked the flag
7026 // into the next HEADERS frame's decode).
7027 debug_assert!(!self.header_reassembly.is_in_progress());
7028 headers
7029 .header_block_fragment
7030 .data(self.zero.storage.buffer())
7031 };
7032 let stream = &mut context.streams[global_stream_id];
7033 let parts = &mut stream.split(&self.position);
7034 let was_initial = parts.rbuffer.is_initial();
7035 let elide_x_real_ip = parts.context.elide_x_real_ip;
7036 let (header_events, status) = pkawa::handle_header(
7037 self.hpack.decoder_mut(),
7038 self.scheduler.prioriser_mut(),
7039 stream_id,
7040 parts.rbuffer,
7041 buffer,
7042 headers.end_stream,
7043 parts.context,
7044 self.flood_detector.config().max_header_list_size(),
7045 self.flood_detector.config().max_header_fields(),
7046 elide_x_real_ip,
7047 );
7048 self.metric_events.extend(header_events);
7049 if self.header_reassembly.is_in_progress() {
7050 self.header_reassembly.finish();
7051 }
7052 self.zero.storage.clear();
7053 if let Err((error, global)) = status {
7054 match self.position {
7055 Position::Client(..) => self
7056 .metric_events
7057 .push(MetricEvent::BackendHeaderParseError),
7058 Position::Server => self
7059 .metric_events
7060 .push(MetricEvent::FrontendHeaderParseError),
7061 }
7062 if global {
7063 error!(
7064 "{} GOT GLOBAL ERROR WHILE PROCESSING HEADERS",
7065 log_context!(self)
7066 );
7067 return self.goaway(error);
7068 } else {
7069 let result =
7070 self.reset_stream(stream_id, global_stream_id, context, endpoint, error);
7071 self.remove_dead_stream(stream_id, global_stream_id);
7072 return result;
7073 }
7074 }
7075 if headers.end_stream {
7076 // RFC 9113 §8.1.1: when END_STREAM arrives via trailers,
7077 // validate that total DATA received matches Content-Length.
7078 // RFC 9110 §8.6: skip for HEAD/1xx/204/304 responses.
7079 if !was_initial && !self.content_length_exempt(&stream.context) {
7080 let parts = stream.split(&self.position);
7081 if let kawa::BodySize::Length(expected) = parts.rbuffer.body_size
7082 && *parts.data_received != expected
7083 {
7084 error!(
7085 "{} Content-Length mismatch on trailers: received {} != declared {}",
7086 log_context!(self),
7087 *parts.data_received,
7088 expected
7089 );
7090 let result = self.reset_stream(
7091 stream_id,
7092 global_stream_id,
7093 context,
7094 endpoint,
7095 H2Error::ProtocolError,
7096 );
7097 self.remove_dead_stream(stream_id, global_stream_id);
7098 return result;
7099 }
7100 }
7101 self.mark_end_of_stream(stream);
7102 }
7103 if let StreamState::Linked(token) = stream.state {
7104 // Mirror of handle_data_frame's rearm. LIFECYCLE invariant 15.
7105 endpoint.readiness_mut(token).arm_writable();
7106 self.metric_events
7107 .push(MetricEvent::WritableRearmedPeerHeaders);
7108 }
7109 if was_initial && self.position.is_client() {
7110 // The backend's response field block has just been decoded and
7111 // parsed: the client twin of the `is_server()` branch below, on
7112 // the same first-HEADERS edge (`was_initial` is what keeps a
7113 // trailer block out of it). This is the whole of
7114 // `backend_header_time`'s H2 arming — nginx's
7115 // `$upstream_header_time` (sozu-proxy/sozu#426).
7116 //
7117 // Unconditional, including on a 1xx: `ConnectionH2::handle_1xx_reset`
7118 // clears the back buffer once the informational response has been
7119 // forwarded, so the FINAL response returns to the initial parsing
7120 // phase and re-enters this edge, overwriting. That is the response
7121 // `SessionMetrics::backend_stop` also anchors on.
7122 stream.metrics.backend_headers_received();
7123 }
7124 // was_initial prevents trailers from triggering connection
7125 if was_initial && self.position.is_server() {
7126 self.metric_events.push(MetricEvent::RequestStarted);
7127 stream.metrics.service_start();
7128 stream.request_counted = true;
7129 stream.state = StreamState::Link;
7130 context.pending_links.push_back(global_stream_id);
7131 }
7132 MuxResult::Continue
7133 }
7134
7135 fn handle_push_promise_frame(&mut self) -> MuxResult {
7136 self.attribute_bytes_to_overhead();
7137 match self.position {
7138 Position::Client(..) => {
7139 // RFC 9113 §8.4: Server push is deprecated. Sozu never sends
7140 // SETTINGS_ENABLE_PUSH=1, so receiving PUSH_PROMISE is a protocol error.
7141 error!(
7142 "{} Received PUSH_PROMISE but server push is not supported",
7143 log_context!(self)
7144 );
7145 self.goaway(H2Error::ProtocolError)
7146 }
7147 Position::Server => {
7148 // Clients must never send PUSH_PROMISE (RFC 9113 §8.4)
7149 error!("{} Received PUSH_PROMISE from client", log_context!(self));
7150 self.goaway(H2Error::ProtocolError)
7151 }
7152 }
7153 }
7154
7155 fn handle_priority_frame<E, L>(
7156 &mut self,
7157 priority: parser::Priority,
7158 context: &mut Context<L>,
7159 endpoint: E,
7160 ) -> MuxResult
7161 where
7162 E: Endpoint,
7163 L: ListenerHandler + L7ListenerHandler,
7164 {
7165 if let Some(global_stream_id) = self
7166 .stream_table
7167 .streams()
7168 .get(&priority.stream_id)
7169 .copied()
7170 {
7171 let stream = &mut context.streams[global_stream_id];
7172 self.attribute_bytes_to_stream(&mut stream.metrics);
7173 } else {
7174 self.attribute_bytes_to_overhead();
7175 }
7176 // Pass 3 Medium #4: standalone PRIORITY frames can arrive for any
7177 // peer-chosen stream ID. Accept only currently-open streams and a
7178 // small idle look-ahead window; everything else is dropped before
7179 // it can feed memory into the priority map.
7180 if self.scheduler.push_priority_guarded(
7181 priority.stream_id,
7182 priority.inner,
7183 self.last_stream_id,
7184 self.stream_table.streams(),
7185 ) {
7186 if let Some(global_stream_id) = self
7187 .stream_table
7188 .streams()
7189 .get(&priority.stream_id)
7190 .copied()
7191 {
7192 let result = self.reset_stream(
7193 priority.stream_id,
7194 global_stream_id,
7195 context,
7196 endpoint,
7197 H2Error::ProtocolError,
7198 );
7199 self.remove_dead_stream(priority.stream_id, global_stream_id);
7200 return result;
7201 } else {
7202 // RFC 7540 §5.3.1 makes a self-dependency a stream error, but
7203 // this stream is idle and RFC 9113 §6.4 forbids RST_STREAM on
7204 // an idle stream: the connection error is the only reply.
7205 error!(
7206 "{} INVALID PRIORITY RECEIVED ON INVALID STREAM",
7207 log_context!(self)
7208 );
7209 return self.goaway(H2Error::ProtocolError);
7210 }
7211 }
7212 MuxResult::Continue
7213 }
7214
7215 /// RFC 9113 §6.3: a PRIORITY frame with a length other than 5 octets is a
7216 /// stream error of type FRAME_SIZE_ERROR. An open stream is reset; on an
7217 /// idle, closed or already reset stream the frame is dropped, since
7218 /// RST_STREAM must not name an idle stream (§6.4) and PRIORITY changes no
7219 /// stream state. The frame's payload was already consumed.
7220 fn handle_priority_frame_size_error<E, L>(
7221 &mut self,
7222 stream_id: StreamId,
7223 context: &mut Context<L>,
7224 endpoint: E,
7225 ) -> MuxResult
7226 where
7227 E: Endpoint,
7228 L: ListenerHandler + L7ListenerHandler,
7229 {
7230 self.metric_events
7231 .push(MetricEvent::FrameReceived(FrameType::Priority));
7232 self.attribute_bytes_to_overhead();
7233 if let Some(global_stream_id) = self.stream_table.get(stream_id)
7234 && context.streams[global_stream_id].state.is_open()
7235 && !self.stream_table.rst_sent_contains(stream_id)
7236 {
7237 debug!(
7238 "{} PRIORITY frame of invalid length on stream {}, sending RST_STREAM(FRAME_SIZE_ERROR)",
7239 log_context!(self),
7240 stream_id
7241 );
7242 let result = self.reset_stream(
7243 stream_id,
7244 global_stream_id,
7245 context,
7246 endpoint,
7247 H2Error::FrameSizeError,
7248 );
7249 self.remove_dead_stream(stream_id, global_stream_id);
7250 return result;
7251 }
7252 debug!(
7253 "{} Ignoring PRIORITY frame of invalid length on stream {}",
7254 log_context!(self),
7255 stream_id
7256 );
7257 self.flood_detector.record_glitch();
7258 check_flood_or_return!(self);
7259 MuxResult::Continue
7260 }
7261
7262 /// RFC 9218 §7.1: PRIORITY_UPDATE reprioritizes an open or idle-soon
7263 /// stream at the connection level. Decodes the priority field value
7264 /// (same grammar as the `priority` request header, `parse_rfc9218_priority`)
7265 /// and pushes it into the `Prioriser` through the same guarded path used
7266 /// for standalone PRIORITY frames — the guard bounds memory against a
7267 /// client spamming PRIORITY_UPDATE for far-future stream IDs.
7268 ///
7269 /// Prioritized stream ID `0` is a connection-level `PROTOCOL_ERROR`
7270 /// (RFC 9218 §7.1). For any other ID that is not currently open or
7271 /// within the idle look-ahead budget, the update is silently dropped
7272 /// (matches the PRIORITY-frame guard semantics — no state change).
7273 fn handle_priority_update_frame(&mut self, pu: parser::PriorityUpdate) -> MuxResult {
7274 self.attribute_bytes_to_overhead();
7275 if pu.prioritized_stream_id == 0 {
7276 error!(
7277 "{} PRIORITY_UPDATE with prioritized_stream_id=0 (RFC 9218 §7.1)",
7278 log_context!(self)
7279 );
7280 return self.goaway(H2Error::ProtocolError);
7281 }
7282 let (urgency, incremental) = pkawa::parse_rfc9218_priority(&pu.priority_field_value);
7283 let (prev_urgency, _) = self.scheduler.priority(&pu.prioritized_stream_id);
7284 trace!(
7285 "{} PRIORITY_UPDATE stream={} urgency={}->{} incremental={} rearmed_writable=true",
7286 log_context!(self),
7287 pu.prioritized_stream_id,
7288 prev_urgency,
7289 urgency,
7290 incremental
7291 );
7292 let _ = self.scheduler.push_priority_guarded(
7293 pu.prioritized_stream_id,
7294 parser::PriorityPart::Rfc9218 {
7295 urgency,
7296 incremental,
7297 },
7298 self.last_stream_id,
7299 self.stream_table.streams(),
7300 );
7301 // LIFECYCLE invariant 15: reprioritisation only changes ordering for
7302 // the NEXT write pass. Under ET epoll, if finalize_write already
7303 // stripped WRITABLE, the scheduler won't re-run without a synthetic
7304 // wake — pair the interest insert with signal_pending_write.
7305 self.readiness.arm_writable();
7306 self.metric_events
7307 .push(MetricEvent::WritableRearmedPriorityUpdate);
7308 MuxResult::Continue
7309 }
7310
7311 fn handle_rst_stream_frame<E, L>(
7312 &mut self,
7313 rst_stream: parser::RstStream,
7314 context: &mut Context<L>,
7315 mut endpoint: E,
7316 ) -> MuxResult
7317 where
7318 E: Endpoint,
7319 L: ListenerHandler + L7ListenerHandler,
7320 {
7321 // Per-error-code counter for the inbound RST. Emitted before the
7322 // flood-detector trip check so even a connection that gets terminated
7323 // by `handle_flood_violation` shows up in the per-code breakdown
7324 // (the dedicated `h2.flood.violation.rst_stream_*` series tracks the
7325 // mitigation event itself).
7326 self.metric_events
7327 .push(MetricEvent::RstStreamReceived(rst_stream.error_code));
7328 // CVE-2023-44487 Rapid Reset + CVE-2019-9514: track RST_STREAM rate.
7329 self.flood_detector.record_rst_stream_window();
7330 check_flood_or_return!(self);
7331 // Additional CVE-2023-44487 mitigation: connection-lifetime caps on
7332 // RST_STREAM frames received. The per-window counter above
7333 // half-decays, so a patient client could keep resetting forever; the
7334 // lifetime counters cap that, relative to the streams a backend
7335 // answered so benign cancels never accumulate toward a fixed ceiling.
7336 // Streams whose backend response has not yet started count toward a
7337 // much lower "abusive" cap (more than half of the streams, past a
7338 // floor) — the Rapid Reset signature, where the attacker pays one RST
7339 // frame and we pay a backend round-trip for each.
7340 //
7341 // "Response started" here means the Server has begun producing
7342 // response bytes (backend kawa buffer past its initial phase). On a
7343 // backend connection (`Position::Client`) the Rapid Reset signature
7344 // does not exist: Sōzu opened the stream, and a backend resetting it
7345 // before answering — `REFUSED_STREAM` under load, a `CANCEL` from its
7346 // own timeout — costs the backend, not Sōzu. Those resets never count
7347 // as pre-response.
7348 //
7349 // On a frontend connection, a stream reset after its response started
7350 // is an answered stream: it counts toward the caps' denominator
7351 // (`routed_to_a_backend`) before the caps are checked, so a client
7352 // that cancels every stream after its response never has one more
7353 // reset than answered streams.
7354 // A stream refused under flood pressure never got a response:
7355 // resetting it is a pre-response reset, whatever the refusal did to
7356 // the stream table. It also shows the peer does not honour
7357 // REFUSED_STREAM, so the soft refusal ends for this connection: more
7358 // refusals would only multiply the RST_STREAM frames Sōzu sends ahead
7359 // of the cap's GOAWAY.
7360 let refused_stream_reset = self.position.is_server()
7361 && self.stream_table.get(rst_stream.stream_id).is_none()
7362 && self
7363 .flood_refused_streams
7364 .is_some_and(|(first, last)| (first..=last).contains(&rst_stream.stream_id));
7365 if refused_stream_reset {
7366 self.flood_refusal_ignored = true;
7367 }
7368 let (response_started, answered) =
7369 match self.stream_table.streams().get(&rst_stream.stream_id) {
7370 Some(global_stream_id) => {
7371 let stream = &context.streams[*global_stream_id];
7372 let started = !stream.back.is_initial();
7373 (
7374 self.position.is_client() || started,
7375 self.position.is_server() && started && routed_to_a_backend(stream),
7376 )
7377 }
7378 None if refused_stream_reset => (false, false),
7379 // Stream already gone (e.g. closed, not yet registered) —
7380 // treat as response-started to avoid over-counting benign
7381 // races as abusive.
7382 None => (true, false),
7383 };
7384 if let Some(violation) =
7385 self.flood_detector
7386 .record_rst_received(response_started, answered, self.now)
7387 {
7388 return self.handle_flood_violation(violation);
7389 }
7390 // Rapid Reset signature (CVE-2023-44487): a RST that arrives before the
7391 // backend has begun answering. Emitted alongside the per-code counter
7392 // so the SOC can alert on the rate of pre-response RSTs without
7393 // having to differentiate by error code.
7394 if !response_started {
7395 self.metric_events
7396 .push(MetricEvent::RstStreamReceivedBeforeResponse);
7397 }
7398 debug!(
7399 "{} RstStream({} -> {})",
7400 log_context!(self),
7401 rst_stream.error_code,
7402 H2Error::try_from(rst_stream.error_code).map_or("UNKNOWN_ERROR", |e| e.as_str())
7403 );
7404 // Compute totals before removing the stream from the map,
7405 // so the removed stream's bytes are included in the total.
7406 let rst_byte_totals = self.compute_stream_byte_totals(context);
7407 if let Some(global_stream_id) = self
7408 .stream_table
7409 .streams()
7410 .get(&rst_stream.stream_id)
7411 .copied()
7412 {
7413 let stream = &mut context.streams[global_stream_id];
7414 self.attribute_bytes_to_stream(&mut stream.metrics);
7415 let linked_token = stream.linked_token();
7416 let (client_rtt, server_rtt) = self.snapshot_rtts(&endpoint, linked_token);
7417 if let Some(token) = linked_token {
7418 endpoint.end_stream(token, global_stream_id, context);
7419 }
7420 let stream = &mut context.streams[global_stream_id];
7421 match &self.position {
7422 // Inbound RST_STREAM on the backend side terminates the in-flight
7423 // request without going through Connection::end_stream (the normal
7424 // place where Backend.active_requests is decremented), so do the
7425 // bookkeeping explicitly here to avoid leaking load counters.
7426 Position::Client(_, backend, BackendStatus::Connected) => {
7427 context.record_backend_delta(backend, BackendChange::StreamsEnded(1));
7428 }
7429 Position::Client(..) => {}
7430 Position::Server => {
7431 self.distribute_overhead(&mut stream.metrics, rst_byte_totals);
7432 // This is a special case, normally, all stream are terminated by the server
7433 // when the last byte of the response is written. Here, the reset is requested
7434 // on the server endpoint and immediately terminates, shortcutting the other path
7435 stream.metrics.backend_stop();
7436 let events = stream.generate_access_log(
7437 true,
7438 Some("H2::ResetFrame"),
7439 context.listener.clone(),
7440 client_rtt,
7441 server_rtt,
7442 );
7443 self.metric_events.extend(events.into_iter().flatten());
7444 stream.state = StreamState::Recycle;
7445 }
7446 }
7447 // Retire from streams/prioriser/stream_last_activity_at and
7448 // invalidate expect_write/expect_read if they reference this gid.
7449 self.remove_dead_stream(rst_stream.stream_id, global_stream_id);
7450 } else {
7451 self.attribute_bytes_to_overhead();
7452 }
7453 MuxResult::Continue
7454 }
7455
7456 fn handle_settings_frame<L>(
7457 &mut self,
7458 settings: parser::Settings,
7459 context: &mut Context<L>,
7460 ) -> MuxResult
7461 where
7462 L: ListenerHandler + L7ListenerHandler,
7463 {
7464 if settings.ack {
7465 // RFC 9113 §6.5: SETTINGS ACK must have empty payload
7466 if !settings.settings.is_empty() {
7467 error!("{} SETTINGS ACK with non-empty payload", log_context!(self));
7468 return self.goaway(H2Error::FrameSizeError);
7469 }
7470 // RFC 9113 §6.5: peer acknowledged our SETTINGS — clear timeout
7471 self.settings_sent_at = None;
7472 // RFC 7541 §4.2: sync the decoder's max allowed table size with
7473 // what we advertised. Currently a no-op (settings don't change at
7474 // runtime), but guards against future runtime SETTINGS updates.
7475 self.hpack.set_decoder_max_allowed_table_size(
7476 self.local_settings.settings_header_table_size as usize,
7477 );
7478 self.attribute_bytes_to_overhead();
7479 return MuxResult::Continue;
7480 }
7481 // CVE-2019-9515: track SETTINGS frame rate
7482 self.flood_detector.record_settings_frame();
7483 check_flood_or_return!(self);
7484 for setting in settings.settings {
7485 let v = setting.value;
7486 let mut is_error = false;
7487 #[rustfmt::skip]
7488 match setting.identifier {
7489 parser::SETTINGS_HEADER_TABLE_SIZE => {
7490 // Cap to the configured maximum — a malicious peer can
7491 // advertise up to 4 GB to inflate HPACK encoder memory.
7492 let cap = self.flood_detector.config().max_header_table_size();
7493 let capped = v.min(cap);
7494 self.peer_settings.settings_header_table_size = capped;
7495 self.hpack.set_encoder_max_table_size(capped as usize);
7496 // RFC 7541 §4.2 / §6.3: queue a dynamic-table-size-update
7497 // HPACK directive for the next header block we emit.
7498 // Without it, the peer's decoder keeps its previous (possibly
7499 // larger) table cap and our encoder-side change is silent
7500 // — conformance suites (h2spec `hpack/4.2`) will flag it.
7501 self.pending_table_size_update = Some(capped);
7502 },
7503 parser::SETTINGS_ENABLE_PUSH => { self.peer_settings.settings_enable_push = v == 1; is_error |= v > 1 },
7504 parser::SETTINGS_MAX_CONCURRENT_STREAMS => { self.peer_settings.settings_max_concurrent_streams = v },
7505 parser::SETTINGS_INITIAL_WINDOW_SIZE => {
7506 // RFC 9113 §6.5.2 / §6.9.2: a value above 2^31-1, or one
7507 // that pushes a stream window past it, is a connection
7508 // error of type FLOW_CONTROL_ERROR, not PROTOCOL_ERROR.
7509 if self.update_initial_window_size(v, context) {
7510 error!("{} INVALID SETTINGS_INITIAL_WINDOW_SIZE {}", log_context!(self), v);
7511 return self.goaway(H2Error::FlowControlError);
7512 }
7513 },
7514 parser::SETTINGS_MAX_FRAME_SIZE => { self.peer_settings.settings_max_frame_size = v; is_error |= !(MIN_MAX_FRAME_SIZE..MAX_MAX_FRAME_SIZE).contains(&v) },
7515 parser::SETTINGS_MAX_HEADER_LIST_SIZE => { self.peer_settings.settings_max_header_list_size = v },
7516 parser::SETTINGS_ENABLE_CONNECT_PROTOCOL => { self.peer_settings.settings_enable_connect_protocol = v == 1; is_error |= v > 1 },
7517 parser::SETTINGS_NO_RFC7540_PRIORITIES => { self.peer_settings.settings_no_rfc7540_priorities = v == 1; is_error |= v > 1 },
7518 other => { warn!("Unknown setting_id: {}, we MUST ignore this", other); self.flood_detector.record_glitch() },
7519 };
7520 if is_error {
7521 error!("{} INVALID SETTING", log_context!(self));
7522 return self.goaway(H2Error::ProtocolError);
7523 }
7524 }
7525
7526 self.attribute_bytes_to_overhead();
7527
7528 // Enlarge the connection-level receive window for backend H2
7529 // connections (Position::Client). The server side serialises it
7530 // beside its own SETTINGS in the `(ClientSettings, Server)` readable
7531 // arm, but the client needs to do it here after receiving the
7532 // server's initial SETTINGS. Once only: a later SETTINGS frame must
7533 // not grant the surplus again (RFC 9113 §6.9.1).
7534 if self.position.is_client() && !self.connection_window_enlarged {
7535 self.connection_window_enlarged = true;
7536 let increment = self
7537 .connection_config
7538 .initial_connection_window
7539 .saturating_sub(DEFAULT_INITIAL_WINDOW_SIZE);
7540 if increment > 0 {
7541 self.queue_window_update(0, increment);
7542 }
7543 // Do NOT increment flow_control.window here: sending our own
7544 // WINDOW_UPDATE enlarges the peer's send allowance, not ours.
7545 // Our send window is only updated by WINDOW_UPDATEs we receive
7546 // from the peer (RFC 9113 §6.9).
7547 }
7548
7549 self.output.push(&serializer::SETTINGS_ACKNOWLEDGEMENT);
7550 self.queue_output();
7551 MuxResult::Continue
7552 }
7553
7554 /// Hand the frame just queued in [`Self::output`] to the write side.
7555 ///
7556 /// Reading goes on: `zero` is input only, so the next frame header no
7557 /// longer lands on queued output. The frame goes out after everything
7558 /// queued before it, including the rest of a stream frame a partial
7559 /// write cut (#1604), as HAProxy appends it to its single output ring.
7560 fn queue_output(&mut self) {
7561 self.readiness.interest.insert(Ready::WRITABLE);
7562 self.readiness.signal_pending_write();
7563 }
7564
7565 /// How much queued output stops reading, as HAProxy's demux stops on a
7566 /// full `mbuf` (`H2_CF_DEM_MROOM`): one buffer's worth. Every queued
7567 /// control frame answers a frame the peer sent, so a peer that sends
7568 /// while never reading cannot grow [`Self::output`] past this.
7569 fn output_read_cap(&self) -> usize {
7570 self.zero.storage.capacity()
7571 }
7572
7573 /// Drop everything queued in [`Self::output`], and restore reading if the
7574 /// read cap had withdrawn it: the only thing reads waited for is gone.
7575 /// Every site that empties the queue goes through here or through
7576 /// [`Self::consume_output`], so `reads_wait_for_output` never outlives
7577 /// the output it waited on.
7578 fn clear_output(&mut self) {
7579 self.output.clear();
7580 if self.reads_wait_for_output {
7581 self.reads_wait_for_output = false;
7582 self.readiness.interest.insert(Ready::READABLE);
7583 }
7584 }
7585
7586 /// Account for `size` bytes the socket took from [`Self::output`], and
7587 /// restore reading once it drains below [`Self::output_read_cap`].
7588 fn consume_output(&mut self, size: usize) {
7589 let overhead = self.output.consume(size);
7590 self.metric_events
7591 .push(self.position.bytes_out_event(overhead));
7592 self.bytes.overhead_bout += overhead;
7593 if self.reads_wait_for_output && self.output.len() < self.output_read_cap() {
7594 self.reads_wait_for_output = false;
7595 self.readiness.interest.insert(Ready::READABLE);
7596 }
7597 }
7598
7599 fn handle_ping_frame(&mut self, ping: parser::Ping) -> MuxResult {
7600 if ping.ack {
7601 self.attribute_bytes_to_overhead();
7602 return MuxResult::Continue;
7603 }
7604 // CVE-2019-9512: track non-ACK PING frame rate
7605 self.flood_detector.record_ping_frame();
7606 check_flood_or_return!(self);
7607 self.attribute_bytes_to_overhead();
7608 let ping_response_size = serializer::PING_ACKNOWLEDGEMENT_HEADER.len() + 8;
7609 match self.output.push_frames(ping_response_size, |buf| {
7610 serializer::gen_ping_acknowledgement(buf, &ping.payload).map(|(_, size)| size)
7611 }) {
7612 Ok(_) => {
7613 self.metric_events.push(MetricEvent::PingAckFrameSent);
7614 }
7615 Err(error) => {
7616 error!(
7617 "{} Could not serialize PingFrame: {:?}",
7618 log_context!(self),
7619 error
7620 );
7621 return self.force_disconnect();
7622 }
7623 };
7624 self.queue_output();
7625 MuxResult::Continue
7626 }
7627
7628 fn handle_goaway_frame<E, L>(
7629 &mut self,
7630 goaway: parser::GoAway,
7631 context: &mut Context<L>,
7632 mut endpoint: E,
7633 ) -> MuxResult
7634 where
7635 E: Endpoint,
7636 L: ListenerHandler + L7ListenerHandler,
7637 {
7638 self.attribute_bytes_to_overhead();
7639 // A peer sends at most two GOAWAY frames in a graceful close (RFC
7640 // 9113 §6.8). Each one counts toward the glitch budget, so a peer
7641 // repeating GOAWAY on a connection kept open by its in-flight
7642 // streams is bounded, as is the log line below.
7643 self.flood_detector.record_glitch();
7644 check_flood_or_return!(self);
7645 let error_name =
7646 H2Error::try_from(goaway.error_code).map_or("UNKNOWN_ERROR", |e| e.as_str());
7647 if goaway.error_code == H2Error::NoError as u32 {
7648 debug!(
7649 "{} Received GOAWAY: last_stream_id={}, error={}, debug_data={:?}",
7650 log_context!(self),
7651 goaway.last_stream_id,
7652 error_name,
7653 goaway.additional_debug_data
7654 );
7655 } else {
7656 // Peer-originated failure: no variant of H2Error from a peer
7657 // implies a sozu bug. On a backend connection, impact handling
7658 // is separate (retry above `last_stream_id`, RST_STREAM for
7659 // consumed streams) and logs its own details below; a frontend
7660 // connection retires nothing. Either way the summary drops to
7661 // `warn!`.
7662 warn!(
7663 "{} Received GOAWAY: last_stream_id={}, error={}, debug_data={:?}",
7664 log_context!(self),
7665 goaway.last_stream_id,
7666 error_name,
7667 goaway.additional_debug_data
7668 );
7669 }
7670 self.metric_events
7671 .push(MetricEvent::GoAwayReceived(goaway.error_code));
7672 // RFC 9113 §6.8: begin graceful drain.
7673 self.drain.observe_peer_goaway(goaway.last_stream_id);
7674 let peer_last_stream_id = self
7675 .drain
7676 .peer_last_stream_id()
7677 .expect("observe_peer_goaway just recorded this");
7678
7679 // RFC 9113 §6.8: `last_stream_id` bounds the streams the GOAWAY
7680 // RECEIVER initiated. On a frontend connection (Position::Server)
7681 // that is server-initiated streams, which sozu never opens (no
7682 // push), so every stream here was opened by the GOAWAY sender and
7683 // is unaffected: each runs to completion, `draining` refuses any new
7684 // one (`create_stream`), and the connection closes once the last
7685 // completes (the `draining && streams().is_empty()` finalize step).
7686 //
7687 // On a backend connection (Position::Client) the streams are
7688 // sozu-initiated: those with ID > last_stream_id were NOT processed
7689 // by the peer and are retried (StreamState::Link) on a new
7690 // connection; those at or below it run to completion.
7691 // IMPORTANT: do NOT call endpoint.end_stream() here — that would
7692 // remove the stream from the frontend's H2 stream map and send
7693 // RST_STREAM to the client, killing the request instead of retrying it.
7694 let mut retry_streams = Vec::new();
7695 if matches!(self.position, Position::Client(..)) {
7696 for (&stream_id, &global_stream_id) in self.stream_table.streams() {
7697 if stream_id > peer_last_stream_id {
7698 retry_streams.push((stream_id, global_stream_id));
7699 }
7700 }
7701 }
7702 for (stream_id, global_stream_id) in &retry_streams {
7703 // `endpoint` is the frontend side (`EndpointServer`) only on a
7704 // backend connection: its `readiness_mut` is what the answers
7705 // below must arm so they reach the client.
7706 debug_assert!(
7707 matches!(self.position, Position::Client(..)),
7708 "only a backend connection retires streams on a received GOAWAY"
7709 );
7710 // Remove from reverse index before transitioning away from Linked.
7711 if let StreamState::Linked(token) = context.streams[*global_stream_id].state {
7712 remove_backend_stream(&mut context.backend_streams, token, *global_stream_id);
7713 }
7714 let stream = &mut context.streams[*global_stream_id];
7715 // A stream above `last_stream_id` was not processed (RFC 9113
7716 // §6.8), but it is only replayable if its request can be sent
7717 // again. Once this connection encoded it, `front.out` holds
7718 // frames bound to this connection's stream id and HPACK encoder
7719 // and its header blocks are gone: re-linked, those frames would
7720 // reach another backend connection of this session, possibly one
7721 // this frontend connection's other streams share (sozu#1632).
7722 if stream.front.consumed {
7723 // Request was already sent to this backend — we can't
7724 // replay it. Use the linked token's readiness (via
7725 // endpoint) so the RST_STREAM reaches the client.
7726 debug!(
7727 "{} GOAWAY: stream {} already consumed, cannot retry",
7728 log_context!(self),
7729 stream_id
7730 );
7731 if let StreamState::Linked(token) = stream.state {
7732 let front_readiness = endpoint.readiness_mut(token);
7733 forcefully_terminate_answer(stream, front_readiness, H2Error::RefusedStream);
7734 } else {
7735 warn!(
7736 "{} GOAWAY: stream {} consumed but not Linked, cannot notify frontend",
7737 log_context!(self),
7738 stream_id
7739 );
7740 }
7741 } else if stream.front_bound_to_backend {
7742 // Encoded for this backend, not sent, not processed: answered
7743 // with a whole 503 response rather than an error-terminated
7744 // one. `forcefully_terminate_answer` leaves an empty response
7745 // kawa in `Error`, which an H2 frontend turns into a
7746 // RST_STREAM but an H1 frontend cannot write at all, so its
7747 // client would wait for its timeout; this core cannot tell
7748 // which frontend it serves. 503, not 502: the backend refused
7749 // the request explicitly and did not process it (RFC 9110
7750 // §15.6.4), which is what a draining GOAWAY means.
7751 debug!(
7752 "{} GOAWAY: stream {} encoded for this backend and unsent, answering 503",
7753 log_context!(self),
7754 stream_id
7755 );
7756 if let StreamState::Linked(token) = stream.state {
7757 let answers_rc = stream.answers.clone();
7758 let answers = answers_rc.borrow();
7759 let front_readiness = endpoint.readiness_mut(token);
7760 set_default_answer(stream, front_readiness, 503, &answers);
7761 debug_assert!(
7762 stream.state == StreamState::Unlinked && !stream.back.is_error(),
7763 "a refused, unsent request gets a whole answer, never a silent error"
7764 );
7765 } else {
7766 warn!(
7767 "{} GOAWAY: stream {} encoded but not Linked, cannot notify frontend",
7768 log_context!(self),
7769 stream_id
7770 );
7771 }
7772 } else {
7773 // Pre: a stream re-linked here carries no frame of this
7774 // connection (sozu-proxy/sozu#1632).
7775 debug_assert!(
7776 stream.front.out.is_empty(),
7777 "a stream re-linked after GOAWAY holds no frame encoded for this connection"
7778 );
7779 stream.state = StreamState::Link;
7780 context.pending_links.push_back(*global_stream_id);
7781 }
7782 // Both retry (!consumed) and terminated (consumed) paths remove the
7783 // stream from self.streams without going through Connection::end_stream,
7784 // so decrement Backend.active_requests here to keep load metrics honest.
7785 if let Position::Client(_, backend, BackendStatus::Connected) = &self.position {
7786 context.record_backend_delta(backend, BackendChange::StreamsEnded(1));
7787 }
7788 // Retire from streams/prioriser/stream_last_activity_at and
7789 // invalidate expect_write/expect_read if they reference this gid.
7790 self.remove_dead_stream(*stream_id, *global_stream_id);
7791 }
7792
7793 // If no active streams remain, close immediately
7794 if self.stream_table.streams().is_empty() {
7795 return self.goaway(H2Error::NoError);
7796 }
7797
7798 // Otherwise, let the remaining streams complete: every stream on a
7799 // frontend connection, those at or below `last_stream_id` on a
7800 // backend one. The connection is closed when all streams finish.
7801 MuxResult::Continue
7802 }
7803
7804 fn handle_window_update_frame<E, L>(
7805 &mut self,
7806 wu: WindowUpdate,
7807 context: &mut Context<L>,
7808 endpoint: E,
7809 ) -> MuxResult
7810 where
7811 E: Endpoint,
7812 L: ListenerHandler + L7ListenerHandler,
7813 {
7814 let stream_id = wu.stream_id;
7815 let increment = wu.increment;
7816
7817 // RFC 9113 §6.9: increment of 0 MUST be treated as an error.
7818 // Connection-level (stream 0) -> connection error (GOAWAY).
7819 // Stream-level -> stream error (RST_STREAM).
7820 if increment == 0 {
7821 if stream_id == 0 {
7822 error!(
7823 "{} WINDOW_UPDATE with zero increment on connection (stream 0)",
7824 log_context!(self)
7825 );
7826 return self.goaway(H2Error::ProtocolError);
7827 } else {
7828 error!(
7829 "{} WINDOW_UPDATE with zero increment on stream {}",
7830 log_context!(self),
7831 stream_id
7832 );
7833 if let Some(global_stream_id) = self.stream_table.get(stream_id) {
7834 let result = self.reset_stream(
7835 stream_id,
7836 global_stream_id,
7837 context,
7838 endpoint,
7839 H2Error::ProtocolError,
7840 );
7841 self.remove_dead_stream(stream_id, global_stream_id);
7842 return result;
7843 }
7844 // Stream not in map (already closed) — treat as glitch
7845 self.flood_detector.record_glitch();
7846 check_flood_or_return!(self);
7847 self.attribute_bytes_to_overhead();
7848 return MuxResult::Continue;
7849 }
7850 }
7851
7852 // The parser masks the reserved bit (STREAM_ID_MASK), so increment <=
7853 // 2^31-1 and try_from always succeeds. Use try_from rather than `as` to
7854 // guard against a future parser change that drops the mask.
7855 let increment = i32::try_from(increment).unwrap_or(i32::MAX);
7856 // RFC 9113 §6.9: a non-zero WINDOW_UPDATE increment is in [1, 2^31-1].
7857 // Zero was short-circuited above; this asserts the masked value is a
7858 // legal positive increment before we add it to a window.
7859 debug_assert!(
7860 increment > 0,
7861 "WINDOW_UPDATE increment must be strictly positive at this point (zero handled above)"
7862 );
7863 if stream_id == 0 {
7864 // Count connection-level WINDOW_UPDATEs before touching the window
7865 // so a per-window flood stops us before we pay the arithmetic cost
7866 // on a million-frame burst. Zero-increment frames short-circuited
7867 // above, so every increment here is a legal-looking rate consumer.
7868 self.flood_detector.record_window_update_stream0();
7869 check_flood_or_return!(self);
7870 self.attribute_bytes_to_overhead();
7871 // Window arithmetic + its replenish-invariant asserts live on
7872 // `H2FlowControl` now (see its module doc); this arm keeps the
7873 // frame-dispatch orchestration — flood accounting, logging,
7874 // GOAWAY — which needs `self` (flood_detector, readiness,
7875 // log_context!) that the flow-control module deliberately does
7876 // not have.
7877 match self.flow_control.apply_window_update(increment) {
7878 h2_flow_control::ApplyWindowUpdateOutcome::Applied {
7879 new_window,
7880 should_arm_writable,
7881 } => {
7882 if should_arm_writable {
7883 self.readiness.arm_writable();
7884 }
7885 debug!(
7886 "{} WINDOW_UPDATE received: stream=0 increment={} new_connection_window={}",
7887 log_context!(self),
7888 increment,
7889 new_window
7890 );
7891 }
7892 h2_flow_control::ApplyWindowUpdateOutcome::Overflow => {
7893 error!("{} INVALID WINDOW INCREMENT", log_context!(self));
7894 return self.goaway(H2Error::FlowControlError);
7895 }
7896 }
7897 } else if let Some(global_stream_id) = self.stream_table.get(stream_id) {
7898 let stream = &mut context.streams[global_stream_id];
7899 self.attribute_bytes_to_stream(&mut stream.metrics);
7900 // RFC 9113 §6.9: only this connection's leg of the stream.
7901 let stream_window = stream.send_window_mut(&self.position);
7902 let stream_window_before = *stream_window;
7903 if let Some(window) = stream_window.checked_add(increment) {
7904 if *stream_window <= 0 && window > 0 {
7905 self.readiness.arm_writable();
7906 }
7907 *stream_window = window;
7908 // Same replenish invariant as the connection window, applied to
7909 // the per-stream send window (RFC 9113 §6.9.1). Overflow past
7910 // 2^31-1 is rejected by `checked_add` and handled as a
7911 // FLOW_CONTROL_ERROR RST_STREAM below.
7912 debug_assert_eq!(
7913 window,
7914 stream_window_before + increment,
7915 "stream window must increase by exactly the increment"
7916 );
7917 debug_assert!(
7918 window > stream_window_before,
7919 "a positive WINDOW_UPDATE must strictly grow the stream window"
7920 );
7921 debug!(
7922 "{} WINDOW_UPDATE received: stream={} increment={} new_stream_window={}",
7923 log_context!(self),
7924 stream_id,
7925 increment,
7926 window
7927 );
7928 } else {
7929 let result = self.reset_stream(
7930 stream_id,
7931 global_stream_id,
7932 context,
7933 endpoint,
7934 H2Error::FlowControlError,
7935 );
7936 self.remove_dead_stream(stream_id, global_stream_id);
7937 return result;
7938 }
7939 } else {
7940 self.attribute_bytes_to_overhead();
7941 trace!(
7942 "{} Ignoring window update on closed stream {}: {}",
7943 log_context!(self),
7944 stream_id,
7945 increment
7946 );
7947 // Pass 3 Low #5: WINDOW_UPDATE on a closed stream is legal
7948 // (RFC 9113 §6.9.1) but has no useful effect, so a peer that
7949 // keeps sending them is wasting our cycles. Count it as a
7950 // glitch so a flood contributes to `check_flood()` and can
7951 // eventually trigger ENHANCE_YOUR_CALM.
7952 self.flood_detector.record_glitch();
7953 check_flood_or_return!(self);
7954 }
7955 MuxResult::Continue
7956 }
7957
7958 /// Apply a peer's `SETTINGS_INITIAL_WINDOW_SIZE` to every stream window
7959 /// this connection holds (RFC 9113 §6.9.2). `true` when the value is
7960 /// invalid — above 2^31-1, or a change that would push some stream
7961 /// window past it — which the caller answers with
7962 /// GOAWAY(FLOW_CONTROL_ERROR). Every window is checked before any is
7963 /// changed, so a rejected value leaves all of them, and the recorded
7964 /// setting, as they were.
7965 fn update_initial_window_size<L>(&mut self, value: u32, context: &mut Context<L>) -> bool
7966 where
7967 L: ListenerHandler + L7ListenerHandler,
7968 {
7969 if value > FLOW_CONTROL_MAX_WINDOW {
7970 return true;
7971 }
7972 let delta = match i32::try_from(
7973 value as i64 - self.peer_settings.settings_initial_window_size as i64,
7974 ) {
7975 Ok(d) => d,
7976 Err(_) => {
7977 error!("{} initial window size delta overflow", log_context!(self));
7978 return true;
7979 }
7980 };
7981 // RFC 9113 §6.9.2: changes to SETTINGS_INITIAL_WINDOW_SIZE can cause
7982 // stream windows to exceed 2^31-1, which is a flow control error.
7983 // Checked for every stream first, so a rejection changes nothing.
7984 // Only streams owned by this connection, and only this connection's
7985 // leg of each (RFC 9113 §6.9): the other connection's window is its
7986 // own.
7987 let overflows = self
7988 .stream_table
7989 .streams()
7990 .values()
7991 .any(|&global_stream_id| {
7992 context.streams[global_stream_id]
7993 .send_window_mut(&self.position)
7994 .checked_add(delta)
7995 .is_none()
7996 });
7997 if overflows {
7998 return true;
7999 }
8000 let mut open_window = false;
8001 for &global_stream_id in self.stream_table.streams().values() {
8002 let stream_window = context.streams[global_stream_id].send_window_mut(&self.position);
8003 let new_window = *stream_window + delta;
8004 open_window |= *stream_window <= 0 && new_window > 0;
8005 *stream_window = new_window;
8006 }
8007 trace!(
8008 "{} UPDATE INIT WINDOW: {} {} {:?}",
8009 log_context!(self),
8010 delta,
8011 open_window,
8012 self.readiness
8013 );
8014 if open_window {
8015 self.readiness.arm_writable();
8016 }
8017 self.peer_settings.settings_initial_window_size = value;
8018 false
8019 }
8020
8021 /// Take the force-disconnect decision as far as it goes without a socket,
8022 /// and park the rest.
8023 ///
8024 /// Everything this function can settle by itself, it settles here:
8025 /// `H2State::Error`, and on a `Position::Client` connection the backend
8026 /// status and the `HUP` readiness event. What it cannot settle is the
8027 /// `tls_wants_write` answer `h2_close::force_disconnect_action` decides
8028 /// on, because the socket belongs to the shell — so it raises an
8029 /// [`H2ForceDisconnectTarget`] and answers provisionally.
8030 ///
8031 /// The provisional answer is exact everywhere but one case.
8032 /// `Position::Client` always continued. A `Position::Server` whose peer is
8033 /// gone always closes, because `h2_close::force_disconnect_action`
8034 /// answers `CloseSession` on `peer_gone` whatever the socket says. Only a
8035 /// server connection with a live peer genuinely turns on the query, and it
8036 /// answers [`MuxResult::Continue`] until the shell settles it — the
8037 /// conservative direction of the two, since a session held one pass too
8038 /// long is recoverable and a session closed with records still buffered is
8039 /// the truncation `h2_close::force_disconnect_action` exists to prevent.
8040 ///
8041 /// Seventeen sites reach this function and every one returns its answer
8042 /// immediately, so on the ordinary paths no core code runs between the
8043 /// raise and the return of the entry point the shell called.
8044 /// [`Self::goaway`] and `Self::send_initial_goaway` are the exception on
8045 /// their GOAWAY-serialisation failure arms, which `Self::enqueue_rst`'s
8046 /// flood escalation can reach from deep in a frame handler; what those
8047 /// paths can observe in the gap is documented on
8048 /// [`Self::force_disconnect_after_query`].
8049 pub fn force_disconnect(&mut self) -> MuxResult {
8050 self.state = H2State::Error;
8051 // Read ONCE above the `match`, for the borrow reason the
8052 // `tls_wants_write` read that used to sit here had: the client arm
8053 // holds `&mut self.position` for its `status` binding, while this
8054 // query borrows all of `self`. Hoisting is behaviour-neutral — it has
8055 // no side effect and the only field it reads that this function writes
8056 // is `self.state`, assigned above — and it is what lets the raised
8057 // target CARRY the answer instead of re-deriving it later. That
8058 // matters: `peer_gone_after_final_goaway` reads `stream_table` and
8059 // `zero.storage`, and `Self::remove_dead_stream` runs between the
8060 // raise and the settlement on the `Self::reset_stream` paths.
8061 let peer_gone = self.peer_gone_after_final_goaway();
8062 match &mut self.position {
8063 Position::Client(_, _, status) => {
8064 *status = BackendStatus::Disconnecting;
8065 self.readiness.event = Ready::HUP;
8066 self.pending_force_disconnect = Some(H2ForceDisconnectTarget::Client);
8067 MuxResult::Continue
8068 }
8069 Position::Server => {
8070 self.pending_force_disconnect = Some(H2ForceDisconnectTarget::Server { peer_gone });
8071 if peer_gone {
8072 MuxResult::CloseSession
8073 } else {
8074 MuxResult::Continue
8075 }
8076 }
8077 }
8078 }
8079
8080 /// Take the [`H2ForceDisconnectTarget`] this connection parked, if it
8081 /// parked one.
8082 ///
8083 /// Taking rather than reading is what makes a second settlement
8084 /// impossible: the next call sees `None` and the shell's helper answers
8085 /// with the result it was already holding.
8086 pub fn poll_force_disconnect(&mut self) -> Option<H2ForceDisconnectTarget> {
8087 self.pending_force_disconnect.take()
8088 }
8089
8090 /// Settle a [`Self::force_disconnect`] with the `tls_wants_write` answer
8091 /// the shell read on its behalf.
8092 ///
8093 /// `tls_wants_write` arrives as this function's own parameter and the
8094 /// target as the other, which is the same discipline
8095 /// [`H2WritableStateTarget::Flush`] keeps by carrying no `bool`: the
8096 /// pre-raise state and the post-raise socket answer are parameters of one
8097 /// function that never read each other's source, and no binding survives
8098 /// in the raising scope for a later edit to point at the wrong question.
8099 /// The query is read fresh here, exactly as [`Self::force_disconnect`]
8100 /// used to read it fresh at all seventeen sites — including the four whose
8101 /// caller already held an answer, which is why none of them passes its own.
8102 ///
8103 /// Between the raise and this call the core cannot change the answer: it
8104 /// holds no socket, so nothing it runs can flush a record. What the gap
8105 /// can change is what the log lines render. On the ordinary paths there is
8106 /// no gap. On [`Self::goaway`]'s serialisation-failure arm, reachable from
8107 /// `Self::enqueue_rst`, `ConnectionH2::remove_dead_stream` and the rest
8108 /// of a frame handler may run first, so `streams=` and `expect_write=`
8109 /// report the connection as the settlement finds it rather than as the
8110 /// raise left it. `peer_gone` is not among them — it is carried.
8111 pub fn force_disconnect_after_query(
8112 &mut self,
8113 target: H2ForceDisconnectTarget,
8114 tls_wants_write: bool,
8115 ) -> MuxResult {
8116 match target {
8117 H2ForceDisconnectTarget::Client => {
8118 debug!(
8119 "{} H2 force_disconnect client: state={:?}, streams={}, expect_write={:?}, wants_write={}, readiness={:?}",
8120 log_context!(self),
8121 self.state,
8122 self.stream_table.streams().len(),
8123 self.stream_table.expect_write(),
8124 tls_wants_write,
8125 self.readiness
8126 );
8127 MuxResult::Continue
8128 }
8129 // Don't disconnect immediately if rustls still has buffered TLS
8130 // records. Returning CloseSession here triggers shutdown(Write)
8131 // which sends FIN — but any TLS records still in rustls's buffer
8132 // (not yet flushed to the TCP send buffer) are lost, causing the
8133 // client to see "TLS decode error / unexpected eof".
8134 // Instead, keep WRITABLE interest and let the writable path flush.
8135 // The decision itself is `h2_close::force_disconnect_action`,
8136 // exhaustively unit-tested there.
8137 //
8138 // The answer is this function's parameter, and reported by every
8139 // log line here. A literal `wants_write=` in any arm is a second
8140 // copy of a fact the socket already owns, and the closing arm is
8141 // reached with records still pending whenever the peer is gone —
8142 // an operator diagnosing a truncation under HAProxy chaining would
8143 // read the opposite of the socket's state.
8144 H2ForceDisconnectTarget::Server { peer_gone } => {
8145 if h2_close::force_disconnect_action(peer_gone, tls_wants_write)
8146 == CloseAction::ReArmAndContinue
8147 {
8148 debug!(
8149 "{} H2 force_disconnect delaying close: state={:?}, streams={}, expect_write={:?}, wants_write={}, readiness={:?}",
8150 log_context!(self),
8151 self.state,
8152 self.stream_table.streams().len(),
8153 self.stream_table.expect_write(),
8154 tls_wants_write,
8155 self.readiness
8156 );
8157 self.readiness.interest = Ready::WRITABLE | Ready::HUP | Ready::ERROR;
8158 self.ensure_tls_flushed(tls_wants_write);
8159 MuxResult::Continue
8160 } else {
8161 debug!(
8162 "{} H2 force_disconnect closing session: state={:?}, streams={}, expect_write={:?}, wants_write={}, readiness={:?}",
8163 log_context!(self),
8164 self.state,
8165 self.stream_table.streams().len(),
8166 self.stream_table.expect_write(),
8167 tls_wants_write,
8168 self.readiness
8169 );
8170 MuxResult::CloseSession
8171 }
8172 }
8173 }
8174 }
8175
8176 /// Reset a stream: tear down kawa state, emit `RST_STREAM` on the wire,
8177 /// and record MadeYouReset accounting.
8178 ///
8179 /// `wire_stream_id` is the on-wire `StreamId`; `stream_id` is the internal
8180 /// `GlobalStreamId` slot. Callers already carry both so we pass them
8181 /// explicitly rather than scanning `self.streams`. The wire id is threaded
8182 /// into `Self::enqueue_rst` which queues the frame for serialisation in
8183 /// `Self::flush_pending_control_frames` on the next writable tick —
8184 /// independent of whether the caller immediately evicts the slot via
8185 /// `remove_dead_stream` (which they usually do). This is what guarantees
8186 /// the RST reaches the peer for malformed HEADERS / flow-control /
8187 /// content-length violations flagged by h2spec 2.0.
8188 pub fn reset_stream<E, L>(
8189 &mut self,
8190 wire_stream_id: StreamId,
8191 stream_id: GlobalStreamId,
8192 context: &mut Context<L>,
8193 mut endpoint: E,
8194 error: H2Error,
8195 ) -> MuxResult
8196 where
8197 E: Endpoint,
8198 L: ListenerHandler + L7ListenerHandler,
8199 {
8200 // Compute totals before taking mutable borrows on the target stream.
8201 let reset_byte_totals = self.compute_stream_byte_totals(context);
8202 context.unlink_stream(stream_id);
8203 let stream = &mut context.streams[stream_id];
8204 trace!(
8205 "{} reset H2 stream {}: {:#?}",
8206 log_context!(self),
8207 stream_id,
8208 stream.context
8209 );
8210 let old_state = std::mem::replace(&mut stream.state, StreamState::Unlinked);
8211 forcefully_terminate_answer(stream, &mut self.readiness, error);
8212 let linked_token = if let StreamState::Linked(token) = old_state {
8213 Some(token)
8214 } else {
8215 None
8216 };
8217 let (client_rtt, server_rtt) = self.snapshot_rtts(&endpoint, linked_token);
8218 if let Some(token) = linked_token {
8219 endpoint.end_stream(token, stream_id, context);
8220 }
8221 // Emit access log for server-side resets on streams that had active requests
8222 if self.position.is_server()
8223 && matches!(old_state, StreamState::Link | StreamState::Linked(_))
8224 {
8225 let stream = &mut context.streams[stream_id];
8226 self.distribute_overhead(&mut stream.metrics, reset_byte_totals);
8227 stream.metrics.backend_stop();
8228 let events = stream.generate_access_log(
8229 true,
8230 Some("H2::Reset"),
8231 context.listener.clone(),
8232 client_rtt,
8233 server_rtt,
8234 );
8235 self.metric_events.extend(events.into_iter().flatten());
8236 stream.metrics.reset();
8237 }
8238 // Queue the RST for wire emission. Independent of the owning stream
8239 // remaining in `self.streams` — callers typically follow this with
8240 // `remove_dead_stream`, which would otherwise evict the slot before
8241 // `write_streams` could run `kawa.prepare` against the converter.
8242 //
8243 // `enqueue_rst` performs every accounting side-effect at queue
8244 // time (per-error counter, global tx counter, CVE-2025-8671
8245 // MadeYouReset cap). Every caller of `reset_stream` answers a frame
8246 // the peer sent (Content-Length mismatch, header parse error,
8247 // PRIORITY self-dependency, zero-increment or overflowing
8248 // WINDOW_UPDATE), so the reset is `RstOrigin::PeerProvoked`; a
8249 // `NoError` reset is still exempt inside the accounting helper.
8250 if let Some(result) = self.enqueue_rst(wire_stream_id, error, RstOrigin::PeerProvoked) {
8251 return result;
8252 }
8253 MuxResult::Continue
8254 }
8255
8256 pub fn end_stream<L>(&mut self, stream_gid: GlobalStreamId, context: &mut Context<L>)
8257 where
8258 L: ListenerHandler + L7ListenerHandler,
8259 {
8260 context.unlink_stream(stream_gid);
8261 let stream_context = context.http_context(stream_gid);
8262 trace!(
8263 "{} end H2 stream {}: {:#?}",
8264 log_context!(self),
8265 stream_gid,
8266 stream_context
8267 );
8268 match self.position {
8269 Position::Client(..) => {
8270 // Resolve the wire StreamId for this gid up front so the
8271 // subsequent cleanup does not hold an iterator borrow on
8272 // `self.streams` while also mutating it.
8273 let wire_stream_id = self
8274 .stream_table
8275 .streams()
8276 .iter()
8277 .find_map(|(&sid, &gid)| (gid == stream_gid).then_some(sid));
8278 if let Some(id) = wire_stream_id {
8279 // Only send RST_STREAM if the stream hasn't fully completed.
8280 // If both request and response are terminated, the stream is
8281 // already in "closed" state (RFC 9113 §5.1) — sending RST_STREAM
8282 // on a closed stream would be a protocol error that could cause
8283 // the H2 peer to close the entire connection.
8284 //
8285 // Nor for a stream the backend has never seen: its id is
8286 // allocated by `start_stream` when the request is linked,
8287 // but its HEADERS leave with a later write pass, and they
8288 // may never have (the preface is not out yet, the pass has
8289 // not run, or the socket took nothing and the block sits
8290 // parked in `front.out`). On `Position::Client`,
8291 // `front.consumed` is exactly "a request byte reached this
8292 // socket", and a stream's first frame is its HEADERS. A
8293 // RST_STREAM on an idle stream is a connection error
8294 // (RFC 9113 §5.1, §6.4), and one queued ahead of the
8295 // preface breaks it (§3.4): either takes down every stream
8296 // this shared connection carries (sozu-proxy/sozu#1631).
8297 // The stream is forgotten instead, as hyperium/h2 drops a
8298 // stream still pending open: its id is burnt, which the
8299 // next HEADERS on a higher id closes implicitly
8300 // (RFC 9113 §5.1.1). A block parked unsent is dropped with
8301 // it and resets the encoder's table on the next pass
8302 // (`ConnectionH2::parked_header_block`).
8303 //
8304 // "Fully completed" means END_STREAM went both ways: the
8305 // response's was read, and the request's was handed to
8306 // the output, which flushes in order. A request received
8307 // whole is not enough: a backend may answer early, before
8308 // the rest of the body reached it (RFC 9113 §8.1), and a
8309 // stream forgotten with its tail unsent stays open on the
8310 // backend, holding one of its MAX_CONCURRENT_STREAMS
8311 // slots (§5.1.2) for this connection's lifetime.
8312 let stream = &context.streams[stream_gid];
8313 let fully_completed = stream.back_received_end_of_stream
8314 && stream.front.is_terminated()
8315 && stream.front.is_completed();
8316 let opened_on_the_wire = stream.front.consumed;
8317 #[cfg(debug_assertions)]
8318 let queued_before = self.output.len();
8319 if opened_on_the_wire
8320 && !fully_completed
8321 && !self.stream_table.rst_sent_contains(id)
8322 {
8323 // Same path as a PING or SETTINGS ACK: queued whole
8324 // behind any output already waiting, including the
8325 // rest of a stream frame a partial write cut, and
8326 // flushed by the next write pass (#1597, #1604).
8327 if self
8328 .output
8329 .push_frames(RST_STREAM_FRAME_SIZE, |buf| {
8330 serializer::gen_rst_stream(buf, id, H2Error::Cancel)
8331 .map(|(_, size)| size)
8332 })
8333 .is_ok()
8334 {
8335 self.metric_events
8336 .push(MetricEvent::RstStreamSent(H2Error::Cancel));
8337 self.queue_output();
8338 self.stream_table.rst_sent_mut().insert(id);
8339 }
8340 }
8341 // Retire the stream and invalidate expect_write/expect_read
8342 // if they still reference this gid — the slot may be popped
8343 // by `shrink_trailing_recycle` on the next create_stream.
8344 self.remove_dead_stream(id, stream_gid);
8345 // Post: a stream that never opened on the wire leaves no
8346 // frame of its own behind (sozu-proxy/sozu#1631).
8347 #[cfg(debug_assertions)]
8348 debug_assert!(
8349 opened_on_the_wire || self.output.len() == queued_before,
8350 "no frame is queued for a backend stream whose HEADERS never left"
8351 );
8352 debug_assert!(
8353 !self.stream_table.streams().contains_key(&id),
8354 "end_stream must retire the backend stream's wire id"
8355 );
8356 if context.streams[stream_gid].state != StreamState::Recycle {
8357 context.streams[stream_gid].state = StreamState::Unlinked;
8358 }
8359 return;
8360 }
8361 error!(
8362 "{} end_stream called for unknown global_stream_id {}",
8363 log_context!(self),
8364 stream_gid
8365 );
8366 }
8367 Position::Server => {
8368 let stream = &mut context.streams[stream_gid];
8369 let answers_rc = stream.answers.clone();
8370 match end_stream_decision(stream) {
8371 EndStreamAction::ForwardTerminated => {
8372 #[cfg(debug_assertions)]
8373 context
8374 .debug
8375 .push(DebugEvent::Str(format!("Close terminated {stream_gid}")));
8376 debug!(
8377 "{} CLOSING H2 TERMINATED STREAM {} {:?}",
8378 log_context!(self),
8379 stream_gid,
8380 stream
8381 );
8382 stream.state = StreamState::Unlinked;
8383 self.readiness.arm_writable();
8384 context.debug.set_interesting(true);
8385 }
8386 EndStreamAction::CloseDelimited
8387 if stream.back.body_size != kawa::BodySize::Empty =>
8388 {
8389 // A `Content-Length` body short of its length, or a chunked body cut before
8390 // its `0\r\n\r\n` or in its trailers, is truncated (RFC 9112 §6.3, §7.1,
8391 // sozu-proxy/sozu#1633): RST_STREAM, never END_STREAM, as does
8392 // `ConnectionH1::terminate_close_delimited`. The blocks not yet encoded go
8393 // with it, among them a trailer block still waiting for
8394 // its closing flags, which the converter would otherwise
8395 // wait on forever; none of them touched the HPACK table
8396 // (sozu-proxy/sozu#1627).
8397 warn!(
8398 "{} H1 backend closed a {:?} response mid-body on H2 stream {}: emitting RST_STREAM",
8399 log_context!(self),
8400 stream.back.body_size,
8401 stream_gid
8402 );
8403 incr!(names::h1::BACKEND_EOF_BEFORE_MESSAGE_COMPLETE);
8404 stream.back.blocks.clear();
8405 stream
8406 .back
8407 .parsing_phase
8408 .error(kawa::ParsingErrorKind::Processing {
8409 message: "INTERNAL_ERROR",
8410 });
8411 stream.state = StreamState::Unlinked;
8412 self.readiness.arm_writable();
8413 context.debug.set_interesting(true);
8414 debug_assert!(
8415 stream.back.is_error() && stream.back.blocks.is_empty(),
8416 "a truncated response ends in Error with nothing left to encode"
8417 );
8418 }
8419 EndStreamAction::CloseDelimited => {
8420 debug!(
8421 "{} CLOSE DELIMITED H2 STREAM {} {:?}",
8422 log_context!(self),
8423 stream_gid,
8424 stream
8425 );
8426 stream.back.push_block(kawa::Block::Flags(kawa::Flags {
8427 end_body: true,
8428 end_chunk: false,
8429 end_header: false,
8430 end_stream: true,
8431 }));
8432 stream.back.parsing_phase = kawa::ParsingPhase::Terminated;
8433 stream.state = StreamState::Unlinked;
8434 self.readiness.arm_writable();
8435 context.debug.set_interesting(true);
8436 }
8437 EndStreamAction::ForwardUnterminated => {
8438 #[cfg(debug_assertions)]
8439 context
8440 .debug
8441 .push(DebugEvent::Str(format!("Close unterminated {stream_gid}")));
8442 debug!(
8443 "{} CLOSING H2 UNTERMINATED STREAM {} {:?}",
8444 log_context!(self),
8445 stream_gid,
8446 stream
8447 );
8448 forcefully_terminate_answer(
8449 stream,
8450 &mut self.readiness,
8451 H2Error::InternalError,
8452 );
8453 context.debug.set_interesting(true);
8454 }
8455 EndStreamAction::SendDefault(status) => {
8456 #[cfg(debug_assertions)]
8457 context.debug.push(DebugEvent::Str(format!(
8458 "Can't retry, send {status} on {stream_gid}"
8459 )));
8460 let answers = answers_rc.borrow();
8461 set_default_answer(stream, &mut self.readiness, status, &answers);
8462 }
8463 EndStreamAction::ReplayOnFreshBackend => {
8464 // Reachable only through an H1 BACKEND behind this H2
8465 // frontend: `reused_from_pool` — and so the captured
8466 // request — is set by `ConnectionH1::start_stream`
8467 // alone, and an H2 backend never fills it. The action
8468 // is handled here rather than merged into
8469 // `SendDefault` so the H2 frontend re-routes the same
8470 // stale-upstream race the H1 frontend does, with the
8471 // same bytes on the wire.
8472 match stream.queue_upstream_replay() {
8473 Some(len) => {
8474 debug!(
8475 "{} H2 REPLAY {} request bytes on a fresh backend",
8476 log_context!(self),
8477 len
8478 );
8479 // Deliberately still a metric macro, and the
8480 // only one left in this core. It is the one
8481 // labelled site: carrying its cluster and
8482 // backend labels in a `MetricEvent` means
8483 // owned `Option<String>`s, which measures at
8484 // `size_of::<MetricEvent>()` 24 -> 48 bytes —
8485 // paid by every queued event, for a site that
8486 // fires only on a stale-upstream retry — and
8487 // costs the enum its `Copy`. Recording it here
8488 // is the cheaper half of that trade, not an
8489 // oversight.
8490 incr!(
8491 names::backend::RETRY_STALE_UPSTREAM,
8492 stream.context.cluster_id.as_deref(),
8493 stream.context.backend_id.as_deref(),
8494 stream.context.cluster_metrics_incarnation()
8495 );
8496 stream.state = StreamState::Link;
8497 context.pending_links.push_back(stream_gid);
8498 }
8499 None => {
8500 error!(
8501 "{} replay selected with no captured request",
8502 log_context!(self)
8503 );
8504 let answers = answers_rc.borrow();
8505 set_default_answer(stream, &mut self.readiness, 502, &answers);
8506 }
8507 }
8508 }
8509 EndStreamAction::Reconnect => {
8510 debug!("{} H2 RECONNECT", log_context!(self));
8511 #[cfg(debug_assertions)]
8512 context
8513 .debug
8514 .push(DebugEvent::Str(format!("Retry {stream_gid}")));
8515 stream.state = StreamState::Link;
8516 context.pending_links.push_back(stream_gid);
8517 }
8518 }
8519 }
8520 }
8521 }
8522
8523 pub fn start_stream<L>(&mut self, stream: GlobalStreamId, context: &mut Context<L>) -> bool
8524 where
8525 L: ListenerHandler + L7ListenerHandler,
8526 {
8527 // Entry point: a backend connection reaches this from the router
8528 // without necessarily having had `readable`/`writable` called on it
8529 // this pass, so adopt the mux's snapshot before arming the new
8530 // stream's liveness deadline below.
8531 self.now = context.now;
8532 // RFC 9113 §6.8: reject new streams on a draining connection
8533 if self.drain.draining() {
8534 error!(
8535 "{} Cannot open new stream on draining connection (stream {})",
8536 log_context!(self),
8537 stream
8538 );
8539 return false;
8540 }
8541 // RFC 9113 §5.1.2: respect peer's max concurrent streams limit
8542 if self.stream_table.len() >= self.peer_settings.settings_max_concurrent_streams as usize {
8543 error!(
8544 "{} Cannot open new stream: active={} >= peer max_concurrent_streams={}",
8545 log_context!(self),
8546 self.stream_table.len(),
8547 self.peer_settings.settings_max_concurrent_streams
8548 );
8549 return false;
8550 }
8551 trace!(
8552 "{} start new H2 stream {} {:?}",
8553 log_context!(self),
8554 stream,
8555 self.readiness
8556 );
8557 // sozu-proxy/sozu#1632, defense in depth: a request whose front
8558 // already holds output was prepared for another connection — frames
8559 // with its stream id and HPACK state, or H1 bytes — and its blocks
8560 // are gone, so this connection could neither send nor re-encode it.
8561 // `end_stream_decision` and `handle_goaway_frame` never link such a
8562 // request; this refuses one that a future path would, before an id is
8563 // allocated. The caller answers the client with an error.
8564 if !context.streams[stream].front.out.is_empty() {
8565 error!(
8566 "{} Cannot open stream {}: its request was already encoded for another connection",
8567 log_context!(self),
8568 stream
8569 );
8570 return false;
8571 }
8572 let Some(stream_id) = self.new_stream_id() else {
8573 // Pass 4 Medium #5: the client-initiated stream-ID space
8574 // (31 bits, odd only) is exhausted. The backend is now useless
8575 // for new requests — gracefully drain it. Without this
8576 // transition, the Connection lingers in `Connected` state and
8577 // every subsequent request returns 503 because `start_stream`
8578 // keeps returning false.
8579 //
8580 // The session envelope is hoisted to a local because the
8581 // `match &mut self.position` below holds a mutable borrow on
8582 // `self.position`, and `log_context!(self)` reads that field
8583 // for its `position={...}` slot — calling the macro inside the
8584 // match arms would conflict with the active borrow. The
8585 // bidirectional regression guard in `lib/tests/log_layout.rs`
8586 // (and the matching scanner in `lib/build.rs`) recognises this
8587 // shape by scanning backward as well as forward from each log
8588 // call.
8589 let context = log_context!(self);
8590 match &mut self.position {
8591 Position::Client(cluster_id, backend, status) => {
8592 let backend_addr = backend.address;
8593 let cluster = cluster_id.clone();
8594 info!(
8595 "{} H2 backend stream IDs exhausted (cluster={}, backend={:?}) — draining",
8596 context, cluster, backend_addr
8597 );
8598 *status = BackendStatus::Disconnecting;
8599 }
8600 Position::Server => {
8601 error!(
8602 "{} H2 server stream IDs exhausted — sending graceful GOAWAY",
8603 context
8604 );
8605 }
8606 }
8607 self.graceful_goaway(self.now);
8608 return false;
8609 };
8610 // RFC 9113 §6.9: the stream's send window on this connection starts
8611 // at the backend's own SETTINGS_INITIAL_WINDOW_SIZE (the 65535
8612 // default until its SETTINGS arrive), whatever the client granted on
8613 // the frontend connection and whatever a previous request on the
8614 // slot left.
8615 *context.streams[stream].send_window_mut(&self.position) =
8616 i32::try_from(self.peer_settings.settings_initial_window_size).unwrap_or(i32::MAX);
8617 // Pre: the backend leg of the slot starts fresh, whether it comes
8618 // from `Context::create_stream` or from an H1 frontend's keep-alive
8619 // reset (sozu-proxy/sozu#1781): nothing has been received on it yet,
8620 // or `handle_read` would refuse the response as arriving on a closed
8621 // stream. The frontend leg may legitimately be done already: an H2
8622 // client's request ends with END_STREAM before the backend is
8623 // dialled.
8624 debug_assert!(
8625 !context.streams[stream].back_received_end_of_stream
8626 && context.streams[stream].back_data_received == 0,
8627 "a stream must open on a backend with a fresh backend leg"
8628 );
8629 self.stream_table.register(stream_id, stream, self.now);
8630 // Sōzu opened this stream; the backend peer cannot inflate the count.
8631 self.flood_detector.record_stream_opened();
8632 self.readiness.arm_writable();
8633 true
8634 }
8635}
8636
8637/// A [`ConnectionH2`] and the socket it speaks through.
8638///
8639/// The split issue #1339 Q10 decided, in the shape it decided: the core is
8640/// byte-in / byte-out and knows nothing about sockets, while everything that
8641/// reaches one lives here. `Connection::H2` holds this rather than the core,
8642/// so `Connection<Front: SocketHandler>` keeps its bound and `Router::backends`
8643/// keeps its declared type — the parameter did not leave the mux, it moved off
8644/// the state machine.
8645///
8646/// Both halves sit in this module, so this impl reaches the core's private
8647/// fields exactly as the methods it inherited used to. Nothing was widened to
8648/// `pub` to let the socket keep working; the `pub` surface that did grow
8649/// exists for a driver OUTSIDE this crate — `sim/tests/h2_simulation.rs` is
8650/// the first, and it builds its own shell over the same poll/handle pairs
8651/// this one uses.
8652pub struct H2Shell<Front: SocketHandler> {
8653 /// The socket-free state machine.
8654 pub core: ConnectionH2,
8655 /// The only socket in the H2 frontend or backend path.
8656 pub socket: Front,
8657 /// `write_streams`' vectored-write scratch, kept for the connection's
8658 /// lifetime so a transmitting pass reuses its capacity instead of
8659 /// allocating it. Empty between two `h2_transmit::gather` /
8660 /// `h2_transmit::confirm` brackets; private so that bracket cannot be
8661 /// split from outside this module.
8662 io_slices: Vec<IoSlice<'static>>,
8663 /// This connection's one sample of [`Self::socket`]'s round-trip time:
8664 /// `None` until the first stream logged on the connection asks for it
8665 /// through [`ShellEndpoint::local_rtt`] (a frontend's `client_rtt`) or
8666 /// `Connection::rtt` (a backend's `server_rtt`), then `Some(sample)` for
8667 /// the rest of the connection's life — see
8668 /// [`memoized_rtt`](super::memoized_rtt). A `Cell` because
8669 /// `Endpoint::local_rtt` takes `&self`.
8670 pub(super) local_rtt: Cell<Option<Option<Duration>>>,
8671 /// A write to the client failed (reset, broken pipe or another socket
8672 /// error): nothing more can be flushed to it, so the connection reports
8673 /// no pending write and `Self::writable` closes the session instead of
8674 /// waiting for a flush that cannot happen (sozu-proxy/sozu#1792).
8675 write_failed: bool,
8676}
8677
8678/// Renders the core and nothing else.
8679///
8680/// Deliberately NOT derived. A derive would render `socket` through
8681/// `Front`'s own `Debug`, which for a `mio::net::TcpStream` is a local
8682/// address, a peer address and a file descriptor — precisely what
8683/// [`ConnectionH2`]'s own `Debug` stopped rendering, and for the same reasons
8684/// it gives. Wrapping the core in a new type must not hand that back through
8685/// the wrapper.
8686impl<Front: SocketHandler> std::fmt::Debug for H2Shell<Front> {
8687 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
8688 self.core.fmt(f)
8689 }
8690}
8691
8692/// Symmetric tear-down for the four aggregate gauges
8693/// `ConnectionH2::gauge_connection_state` feeds — the three
8694/// `h2.connection.*` metrics and `h2.streams.ready_incremental.by_urgency`:
8695/// whatever positive contribution this connection made is subtracted back out
8696/// when the connection is dropped, and the whole event queue is flushed with
8697/// it.
8698///
8699/// Using `Drop` (rather than wiring decrements into every close path —
8700/// `graceful_goaway`, `force_disconnect`, `handle_goaway_frame`, `Mux::close`,
8701/// stream-id exhaustion, panic-unwind) is what guarantees the gauge is
8702/// arithmetically symmetric regardless of which path teardown took. Past
8703/// underflow incidents (commits ff401b54, aadb3fa4) were increment/decrement
8704/// asymmetries. `ff401b54` carried both shapes at once: a `-1` for streams that
8705/// never ran the `+1`, AND a `-1` that ran twice on one stream (100-Continue,
8706/// and an H2 reset followed by close). `aadb3fa4` was the doubled `-1` alone,
8707/// let through by an early return. `Drop` closes both shapes — it is the sole
8708/// decrement site, and taking `last_gauge_snapshot` makes a second call a
8709/// no-op.
8710///
8711/// It sits on the **shell** rather than on [`ConnectionH2`] because
8712/// `record_metric` writes the worker-local `METRICS` aggregator, which is
8713/// exactly the ambient reach the core gave up. RAII is not given up with it:
8714/// the shell owns the core, so the core cannot outlive this, and because
8715/// `H2Shell` now implements `Drop` the compiler additionally refuses to move
8716/// `core` out of it. If this impl were lost, every live connection's
8717/// contribution would leak upward permanently, with no resync and no
8718/// underflow to notice — which is what
8719/// `dropping_a_connection_releases_its_ready_incremental_contribution` pins.
8720/// The [`Endpoint`] the shell hands its core: every call forwards to `inner`,
8721/// except [`Endpoint::local_rtt`], which the shell answers from its own socket.
8722///
8723/// The core asks for the local round-trip time only when it logs a stream
8724/// (`ConnectionH2::snapshot_rtts`), so the `getsockopt(TCP_INFO)` happens then
8725/// and only then, once per connection: the first ask samples and stores into
8726/// `sample`, every later one on the connection reuses it. Built per core
8727/// call from disjoint borrows of the shell's fields, so the core never holds
8728/// the socket. Issue #1590.
8729struct ShellEndpoint<'a, E, Front> {
8730 inner: &'a mut E,
8731 socket: &'a Front,
8732 sample: &'a Cell<Option<Option<Duration>>>,
8733}
8734
8735impl<'a, E, Front> ShellEndpoint<'a, E, Front> {
8736 fn new(
8737 inner: &'a mut E,
8738 socket: &'a Front,
8739 sample: &'a Cell<Option<Option<Duration>>>,
8740 ) -> Self {
8741 ShellEndpoint {
8742 inner,
8743 socket,
8744 sample,
8745 }
8746 }
8747}
8748
8749impl<E: Endpoint, Front: SocketHandler> std::fmt::Debug for ShellEndpoint<'_, E, Front> {
8750 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
8751 f.debug_struct("ShellEndpoint")
8752 .field("inner", &self.inner)
8753 .field("sample", &self.sample.get())
8754 .finish()
8755 }
8756}
8757
8758impl<E: Endpoint, Front: SocketHandler> Endpoint for ShellEndpoint<'_, E, Front> {
8759 fn readiness(&self, token: mio::Token) -> &Readiness {
8760 self.inner.readiness(token)
8761 }
8762 fn readiness_mut(&mut self, token: mio::Token) -> &mut Readiness {
8763 self.inner.readiness_mut(token)
8764 }
8765 fn peer_rtt(&self, token: mio::Token) -> Option<Duration> {
8766 self.inner.peer_rtt(token)
8767 }
8768 fn local_rtt(&self) -> Option<Duration> {
8769 super::memoized_rtt(self.sample, self.socket.socket_ref())
8770 }
8771 fn end_stream<L: ListenerHandler + L7ListenerHandler>(
8772 &mut self,
8773 token: mio::Token,
8774 stream: GlobalStreamId,
8775 context: &mut Context<L>,
8776 ) {
8777 self.inner.end_stream(token, stream, context)
8778 }
8779 fn start_stream<L: ListenerHandler + L7ListenerHandler>(
8780 &mut self,
8781 token: mio::Token,
8782 stream: GlobalStreamId,
8783 context: &mut Context<L>,
8784 ) -> bool {
8785 self.inner.start_stream(token, stream, context)
8786 }
8787}
8788
8789impl<Front: SocketHandler> Drop for H2Shell<Front> {
8790 fn drop(&mut self) {
8791 self.core.release_connection_gauges();
8792 self.record_pending_metrics();
8793 }
8794}
8795
8796impl<Front: SocketHandler> H2Shell<Front> {
8797 /// The TLS layer is holding encrypted records — handshake, alert, session
8798 /// ticket or already-accepted application data — that it must push before
8799 /// this connection can call a pass finished.
8800 ///
8801 /// **Not "the socket is writable".** The name of the underlying
8802 /// [`SocketHandler`] method says socket, and that is what made this look
8803 /// like an I/O question for as long as it was spelled out at every site.
8804 /// It is a question about a buffer that happens to live behind the socket:
8805 /// `FrontRustls` is the only production handler that overrides it, the
8806 /// trait's default answer is `false`, and `Router::backends` is declared
8807 /// `Connection<SessionTcpStream>` whatever the frontend is — so every
8808 /// backend H2 connection already resolves every one of these queries
8809 /// statically to `false`. The same core is monomorphised twice today, once
8810 /// with TLS and once without, which is the evidence that the coupling is
8811 /// incidental rather than structural.
8812 ///
8813 /// This is a seam, not an abbreviation. It is the single reach where
8814 /// `ConnectionH2` asks that question, and the whole of what a byte-in /
8815 /// byte-out core has to receive as an input instead of querying: the shell
8816 /// that owns the socket is the only layer that can answer it, and it is
8817 /// the layer this method's body moves to. Until then the body is the
8818 /// delegation it always was and the answer is bit-identical.
8819 ///
8820 /// The query is free of side effects, so a caller that needs it twice
8821 /// around a [`Self::flush_tls_records`] is asking two genuinely different
8822 /// questions — "does rustls hold records" and "did the kernel take them" —
8823 /// and a caller that needs it twice with nothing in between should bind it
8824 /// once instead. `h2_close::TlsFlushPhase` names the first case;
8825 /// [`ConnectionH2::force_disconnect`] is the second.
8826 fn tls_wants_write(&self) -> bool {
8827 self.socket.socket_wants_write()
8828 }
8829
8830 /// Ask the TLS layer to push whatever it has buffered towards the kernel,
8831 /// offering it no new application bytes.
8832 ///
8833 /// The empty slice is the whole point: this is a flush, not a write. It is
8834 /// the only *action* among the three TLS seams, which is why the four call
8835 /// sites are the four places where a byte-in / byte-out core must hand
8836 /// control back to its shell rather than take an input — the answer to
8837 /// [`Self::tls_wants_write`] changes across this call, so no value captured
8838 /// before it can stand in for the query after it.
8839 ///
8840 /// Three of the four have made that move and all three are in the shell
8841 /// impl: [`Self::write_streams`] performs it for
8842 /// [`H2FinalizeTarget::Flush`], outside the [`ConnectionH2::finalize_write`] that
8843 /// used to run it inline, and [`Self::writable`] performs both of the
8844 /// connection-level write pass's — its unconditional preamble, and the
8845 /// second one [`H2WritableStateTarget::Flush`] asks for on behalf of the
8846 /// `H2State::GoAway` arm now in [`ConnectionH2::dispatch_writable_state`]. The
8847 /// fourth is [`Self::flush_output_buffer`].
8848 ///
8849 /// Returns the handler's `(size, SocketResult)` unchanged. Three of the
8850 /// four callers discard both and re-query [`Self::tls_wants_write`]
8851 /// instead — deliberately, since a flush that moved bytes may still leave
8852 /// records behind — and only [`Self::flush_output_buffer`] consumes the
8853 /// status, through `update_readiness_after_write`.
8854 fn flush_tls_records(&mut self) -> (usize, SocketResult) {
8855 let (size, status) = self.socket.socket_write(&[]);
8856 self.core.note_write_status(status);
8857 self.record_write(status);
8858 (size, status)
8859 }
8860
8861 /// Remember a failed write to the client: an H2 frontend whose socket
8862 /// answered `Error` or `Closed` can flush nothing more. A backend keeps
8863 /// its own failure handling.
8864 fn record_write(&mut self, status: SocketResult) {
8865 if self.core.position.is_server()
8866 && matches!(status, SocketResult::Error | SocketResult::Closed)
8867 {
8868 self.write_failed = true;
8869 }
8870 }
8871
8872 /// Start the TLS `close_notify` handshake, generating the records that
8873 /// [`Self::tls_wants_write`] will then report as pending.
8874 ///
8875 /// Idempotence is the caller's business, not this seam's:
8876 /// [`Self::initiate_close_notify`] gates it on `close_notify_sent` so a
8877 /// second call cannot queue a second alert.
8878 fn begin_tls_close(&mut self) {
8879 self.socket.socket_close()
8880 }
8881
8882 /// Start TLS close_notify on the frontend and keep the session alive until
8883 /// rustls has flushed the generated records.
8884 pub fn initiate_close_notify(&mut self) -> bool {
8885 if !self.core.position.is_server()
8886 || matches!(
8887 self.core.state,
8888 H2State::ClientPreface | H2State::ClientSettings | H2State::ServerSettings
8889 )
8890 {
8891 return false;
8892 }
8893 if !self.core.close_notify_sent {
8894 trace!("{} H2 initiating CLOSE_NOTIFY", log_context!(self.core));
8895 self.begin_tls_close();
8896 self.core.close_notify_sent = true;
8897 }
8898 let tls_wants_write = self.tls_wants_write();
8899 if tls_wants_write {
8900 self.core.readiness.interest = Ready::WRITABLE | Ready::HUP | Ready::ERROR;
8901 self.core.ensure_tls_flushed(tls_wants_write);
8902 true
8903 } else {
8904 false
8905 }
8906 }
8907
8908 /// Returns `true` if there is data queued waiting to be flushed:
8909 /// - The ordered output queue: control frames (GOAWAY, SETTINGS ACK,
8910 /// etc.) and the rest of a stream frame a stalled write cut
8911 /// - A stream parked by a stalled write (`expect_write`)
8912 /// - Encrypted TLS records in rustls's output buffer not yet flushed to TCP
8913 ///
8914 /// The TLS check is critical: `shutting_down()` uses this to prevent
8915 /// premature session close while response DATA is still in rustls's
8916 /// buffer (accepted by `socket_write_vectored` but not yet on the wire).
8917 ///
8918 /// Does NOT check per-stream `back.out`/`back.blocks`; use
8919 /// [`Self::has_pending_write_full`] on paths that must honour
8920 /// LIFECYCLE invariant 16 (e.g. shutdown-drain).
8921 pub fn has_pending_write(&self) -> bool {
8922 !self.write_failed && self.core.has_pending_write_with(self.tls_wants_write())
8923 }
8924
8925 /// Connection-level [`Self::has_pending_write`] extended with a per-stream
8926 /// back-buffer probe (LIFECYCLE §9 invariant 16). Used by shutdown-drain
8927 /// paths that must not close while any open stream still has outbound
8928 /// kawa bytes queued — a voluntary scheduler yield can leave `back.out`
8929 /// or `back.blocks` non-empty without `expect_write` being set.
8930 pub fn has_pending_write_full<L>(&self, context: &Context<L>) -> bool
8931 where
8932 L: ListenerHandler + L7ListenerHandler,
8933 {
8934 !self.write_failed
8935 && (self.has_pending_write()
8936 || any_stream_has_pending_back(self.core.stream_table.streams(), &context.streams))
8937 }
8938
8939 /// Flush the ordered output queue to the socket, counting its own bytes
8940 /// as connection overhead.
8941 ///
8942 /// Returns `true` if the socket stalled (WouldBlock / zero-length write),
8943 /// meaning the caller should stop writing and wait for the next writable event.
8944 /// Returns `false` when the queue has been fully drained.
8945 fn flush_output_to_socket(&mut self) -> bool {
8946 while !self.core.output_pending().is_empty() {
8947 // The final GOAWAY takes the `close_notify` with it: one
8948 // `write_tls` instead of this one and `H2Shell::close`'s.
8949 let (size, status) = if self.core.output_flush_closes_connection() {
8950 let pending = self.core.output_pending().len();
8951 let (size, status) = self
8952 .socket
8953 .socket_write_then_close(self.core.output_pending());
8954 if size == pending {
8955 self.core.close_notify_sent = true;
8956 }
8957 (size, status)
8958 } else {
8959 self.socket.socket_write(self.core.output_pending())
8960 };
8961 self.record_write(status);
8962 #[cfg(debug_assertions)]
8963 trace!(
8964 "{} flush_output_to_socket: written={}, status={:?}, wants_write={}",
8965 log_context!(self.core),
8966 size,
8967 status,
8968 self.tls_wants_write()
8969 );
8970 if self.core.consume_output_flush(size, status) {
8971 return true;
8972 }
8973 }
8974 false
8975 }
8976
8977 /// Directly flush the ordered output queue to the socket without going
8978 /// through the full writable() path. Used during shutdown when the event
8979 /// loop won't deliver new epoll events for this session (edge-triggered).
8980 ///
8981 /// Whatever is queued can go at any time: every entry is a whole frame
8982 /// (or the rest of one a stalled write cut), queued in wire order, and
8983 /// the queue shares nothing with the read side (#1604). A stream parked
8984 /// by a stalled write keeps its park; only its already-started frame
8985 /// lives in the queue.
8986 pub fn flush_output_buffer(&mut self) {
8987 if self.flush_output_to_socket() {
8988 return;
8989 }
8990 if self.tls_wants_write() {
8991 let (_size, status) = self.flush_tls_records();
8992 let _ = update_readiness_after_write(0, status, &mut self.core.readiness);
8993 }
8994 }
8995
8996 pub fn close<E, L>(&mut self, context: &mut Context<L>, mut endpoint: E)
8997 where
8998 E: Endpoint,
8999 L: ListenerHandler + L7ListenerHandler,
9000 {
9001 match self.core.position {
9002 Position::Client(_, _, BackendStatus::KeepAlive) => {
9003 error!(
9004 "{} H2 connections do not use KeepAlive backend status",
9005 log_context!(self.core)
9006 );
9007 return;
9008 }
9009 Position::Client(..) => {}
9010 Position::Server => {
9011 let tls_pending_before = self.tls_wants_write();
9012 if !self.core.stream_table.streams().is_empty()
9013 || tls_pending_before
9014 || self.core.stream_table.expect_write().is_some()
9015 {
9016 debug!(
9017 "{} H2 close with active state: state={:?}, streams={}, expect_write={:?}, wants_write={}, readiness={:?}",
9018 log_context!(self.core),
9019 self.core.state,
9020 self.core.stream_table.streams().len(),
9021 self.core.stream_table.expect_write(),
9022 tls_pending_before,
9023 self.core.readiness
9024 );
9025 for (stream_id, global_stream_id) in self.core.stream_table.streams() {
9026 let stream = &context.streams[*global_stream_id];
9027 debug!(
9028 "{} close stream id={} gid={}: state={:?}, front_eos={}, back_eos={}, front_phase={:?}, back_phase={:?}, front_completed={}, back_completed={}",
9029 log_context!(self.core),
9030 stream_id,
9031 global_stream_id,
9032 stream.state,
9033 stream.front_received_end_of_stream,
9034 stream.back_received_end_of_stream,
9035 stream.front.parsing_phase,
9036 stream.back.parsing_phase,
9037 stream.front.is_completed(),
9038 stream.back.is_completed()
9039 );
9040 }
9041 }
9042 if !self.core.close_notify_sent {
9043 trace!("{} H2 SENDING CLOSE NOTIFY", log_context!(self.core));
9044 }
9045 let (tls_pending_after, drain_rounds) =
9046 drain_tls_close_notify(&mut self.socket, &mut self.core.close_notify_sent);
9047 if tls_pending_after {
9048 // Severity tiering: key on stream-count + close-state, not
9049 // peer-vs-operator. Composes with the send-side `H2Error`
9050 // variant tier in `goaway()` — both rules demote benign
9051 // paths and keep loss-bearing paths loud.
9052 //
9053 // - `streams != 0` -> `error!`: live streams at
9054 // close time, response-byte loss is possible.
9055 // - `streams == 0` AND state in {GoAway, Error}
9056 // -> `warn!`: idle close after
9057 // a GOAWAY exchange (peer-initiated abort or our own
9058 // graceful drain). What's stranded is best-effort
9059 // GOAWAY/close_notify; no application data was queued.
9060 // - `streams == 0` from any other state
9061 // -> `error!`: unexpected
9062 // teardown path (no GOAWAY exchange) — keep loud so
9063 // unknown failure modes surface.
9064 if !self.core.stream_table.streams().is_empty() {
9065 error!(
9066 "{} TLS buffer NOT fully drained on close: \
9067 pending_before={}, pending_after={}, drain_rounds={}, \
9068 state={:?}, streams={}, expect_write={:?}, \
9069 close_notify_sent={}, readiness={:?}",
9070 log_context!(self.core),
9071 tls_pending_before,
9072 tls_pending_after,
9073 drain_rounds,
9074 self.core.state,
9075 self.core.stream_table.streams().len(),
9076 self.core.stream_table.expect_write(),
9077 self.core.close_notify_sent,
9078 self.core.readiness
9079 );
9080 } else if matches!(self.core.state, H2State::GoAway | H2State::Error) {
9081 warn!(
9082 "{} TLS buffer NOT fully drained on close: \
9083 pending_before={}, pending_after={}, drain_rounds={}, \
9084 state={:?}, streams={}, expect_write={:?}, \
9085 close_notify_sent={}, readiness={:?}",
9086 log_context!(self.core),
9087 tls_pending_before,
9088 tls_pending_after,
9089 drain_rounds,
9090 self.core.state,
9091 self.core.stream_table.streams().len(),
9092 self.core.stream_table.expect_write(),
9093 self.core.close_notify_sent,
9094 self.core.readiness
9095 );
9096 } else {
9097 error!(
9098 "{} TLS buffer NOT fully drained on close: \
9099 pending_before={}, pending_after={}, drain_rounds={}, \
9100 state={:?}, streams={}, expect_write={:?}, \
9101 close_notify_sent={}, readiness={:?}",
9102 log_context!(self.core),
9103 tls_pending_before,
9104 tls_pending_after,
9105 drain_rounds,
9106 self.core.state,
9107 self.core.stream_table.streams().len(),
9108 self.core.stream_table.expect_write(),
9109 self.core.close_notify_sent,
9110 self.core.readiness
9111 );
9112 }
9113 }
9114 return;
9115 }
9116 }
9117 // reconnection is handled by the server for each stream separately
9118 for global_stream_id in self.core.stream_table.streams().values() {
9119 trace!(
9120 "{} end stream: {}",
9121 log_context!(self.core),
9122 global_stream_id
9123 );
9124 if let StreamState::Linked(token) = context.streams[*global_stream_id].state {
9125 endpoint.end_stream(token, *global_stream_id, context);
9126 }
9127 }
9128 }
9129
9130 /// Wrap a freshly built core around the socket it will speak through.
9131 ///
9132 /// The `peer_addr()` call is here because this is the layer that has a
9133 /// socket to ask. It is still the one read of it on the connection's whole
9134 /// lifetime — `ConnectionH2::peer_address` documents why the answer is
9135 /// snapshotted rather than re-read — and it now happens one layer out,
9136 /// before the core exists rather than inside its constructor.
9137 #[allow(clippy::too_many_arguments)]
9138 pub fn new(
9139 session_ulid: Ulid,
9140 socket: Front,
9141 position: super::Position,
9142 buffers: &mut dyn super::buffer_source::BufferSource,
9143 flood_config: H2FloodConfig,
9144 connection_config: H2ConnectionConfig,
9145 stream_idle_timeout: std::time::Duration,
9146 graceful_shutdown_deadline: Option<std::time::Duration>,
9147 timeout_duration: std::time::Duration,
9148 expect_read: Option<(H2StreamId, usize)>,
9149 readiness_interest: sozu_command::ready::Ready,
9150 ) -> Option<Self> {
9151 let core = ConnectionH2::new(
9152 session_ulid,
9153 socket.peer_addr(),
9154 position,
9155 buffers,
9156 flood_config,
9157 connection_config,
9158 stream_idle_timeout,
9159 graceful_shutdown_deadline,
9160 timeout_duration,
9161 expect_read,
9162 readiness_interest,
9163 )?;
9164 Some(H2Shell {
9165 core,
9166 socket,
9167 io_slices: Vec::new(),
9168 local_rtt: Cell::new(None),
9169 write_failed: false,
9170 })
9171 }
9172}
9173
9174/// The socket shell of the H2 data path.
9175///
9176/// Every production `self.socket` in this file is in this block and its
9177/// sibling above it, and `ConnectionH2` has none. `H2Shell::readable` and
9178/// `H2Shell::write_streams` are the two byte movers: each is the caller that
9179/// sits between one of the core's two poll/handle pairs —
9180/// [`ConnectionH2::poll_read_target`] with [`ConnectionH2::handle_read`], and
9181/// [`ConnectionH2::poll_write_target`] with [`ConnectionH2::handle_write`].
9182/// All four are `pub`, and so are the buffers they name
9183/// ([`ConnectionH2::read_space`], [`ConnectionH2::write_buffer`]), so a driver
9184/// outside this crate can stand exactly where these two functions stand.
9185/// `sim/tests/h2_simulation.rs` is that driver, and it is what makes the claim
9186/// measurable rather than asserted.
9187///
9188/// `H2Shell::writable` is the third member, and the only socket it touches it
9189/// touches through the three TLS seams — `H2Shell::tls_wants_write`,
9190/// `H2Shell::flush_tls_records` and `H2Shell::begin_tls_close` — whose bodies
9191/// live here too. It drives two more poll/handle pairs:
9192/// [`ConnectionH2::flush_pending_control_frames`] with
9193/// [`ConnectionH2::handle_control_flush`], and
9194/// [`ConnectionH2::dispatch_writable_state`] with
9195/// [`ConnectionH2::dispatch_writable_state_after_flush`]. Neither takes a
9196/// `Front`: the `(H2State, Position)` dispatch and the control-frame walk are
9197/// both drivable without a socket, which is the whole claim.
9198///
9199/// The fourth pair is `ConnectionH2::force_disconnect` with
9200/// [`ConnectionH2::force_disconnect_after_query`], and it is the one that
9201/// cannot ride a return value — see [`H2ForceDisconnectTarget`]. `H2Shell::settled`
9202/// is where it lands, and every `MuxResult` a core step hands this block goes
9203/// through it.
9204///
9205/// # Why `write_streams` stays private, and where the `unsafe` windows are
9206///
9207/// `H2Shell::write_streams` drives the `Vec<IoSlice<'static>>` that brackets
9208/// `h2_transmit::gather` and `h2_transmit::confirm`, and it drives it because
9209/// the loop that opens and closes that bracket lives with it. The vector
9210/// itself is the shell's private `io_slices` field, so its capacity outlives
9211/// the pass while no descriptor does. There are TWO
9212/// `unsafe` blocks on this path and they nest:
9213///
9214/// - the INNER one is `gather`'s `slice::from_raw_parts`, which re-labels
9215/// borrows of `kawa.storage` as `'static`. It opens and closes inside that
9216/// function, exactly as it always did.
9217/// - the OUTER one is the `unsafe { … }` around the `gather` CALL below, which
9218/// exists because `gather` is `pub unsafe fn`: the lifetime obligation is
9219/// stated in its signature and discharged by the caller. It wraps the call
9220/// and nothing else — not the vectored write, not the `confirm`. It is the
9221/// only `unsafe` block in production code in this file.
9222///
9223/// **The window did not widen when `write_streams` moved onto `H2Shell`, nor
9224/// when the vector moved into a field of it.** It still opens at that `gather`
9225/// call and closes at the `confirm` three statements later, where
9226/// `io_slices.clear()` runs before the consume which may relocate the storage.
9227/// The only thing entered while the descriptors are live is
9228/// `SocketHandler::socket_write_vectored`, which reborrows them for the
9229/// duration of the call and cannot retain them. No `IoSlice<'static>`
9230/// survives one iteration of the loop: what the field keeps between passes is
9231/// capacity, and the vector is empty whenever `write_streams` is not inside
9232/// that bracket. `gather` clears it on entry as well, so even a descriptor
9233/// stranded by a panic out of the vectored write is discarded before anything
9234/// could read it. What changed is the
9235/// receiver, from `&mut ConnectionH2<Front>` to `&mut H2Shell<Front>`, and the
9236/// buffer now arrives through [`ConnectionH2::write_buffer`] instead of the
9237/// free function — neither of which moves either end of the bracket.
9238///
9239/// That the bracket does not span the poll/handle seam matters MORE now, not
9240/// less: the seam is `pub`, the buffers it names are `pub`, and an
9241/// out-of-crate driver really does exist, so halves on opposite sides of it
9242/// could be separated by a caller this crate cannot see. `write_streams` stays
9243/// private for that reason, on `H2Shell` exactly as it was on `ConnectionH2`.
9244/// `h2_transmit`'s pair is exported — `gather` as `pub unsafe fn`, `confirm`
9245/// as a plain `pub fn` that produces no descriptor and so leaves no
9246/// obligation — which moves the same argument onto that driver's own bracket
9247/// rather than dissolving it. `H2Harness::write_streams` in
9248/// `sim/tests/h2_simulation.rs` opens and closes its own bracket inside its
9249/// own single function, for the same reason and with the same shape.
9250impl<Front: SocketHandler> H2Shell<Front> {
9251 /// Answer a core step, settling any [`H2ForceDisconnectTarget`] it parked
9252 /// on the way.
9253 ///
9254 /// The one place the socket answers a force-disconnect, and the reason
9255 /// `ConnectionH2::force_disconnect` can be raised from seventeen sites
9256 /// that hold no socket. Every `MuxResult` a core step hands this impl
9257 /// block leaves through here, which is what makes the discipline
9258 /// greppable rather than asserted: a core call in this block whose type is
9259 /// [`MuxResult`] and which does not appear as this function's argument is
9260 /// a missed settlement.
9261 ///
9262 /// `result` is the core's provisional answer and survives untouched when
9263 /// nothing was parked — the overwhelmingly common case, since only a
9264 /// force-disconnect parks anything.
9265 /// [`ConnectionH2::force_disconnect`], settled.
9266 ///
9267 /// The public spelling of the decision: the core raises it and this reads
9268 /// the query it could not. Every out-of-shell caller goes through here.
9269 pub fn force_disconnect(&mut self) -> MuxResult {
9270 let result = self.core.force_disconnect();
9271 self.settled(result)
9272 }
9273
9274 /// [`ConnectionH2::graceful_goaway`], settled.
9275 ///
9276 /// Reaches `ConnectionH2::force_disconnect` through
9277 /// `ConnectionH2::send_initial_goaway`'s serialisation-failure arm.
9278 pub fn graceful_goaway(&mut self, now: Instant) -> MuxResult {
9279 let result = self.core.graceful_goaway(now);
9280 self.settled(result)
9281 }
9282
9283 /// [`ConnectionH2::goaway_before_forced_close`], settled.
9284 ///
9285 /// Reaches `ConnectionH2::force_disconnect` through the
9286 /// serialisation-failure arm of the final GOAWAY.
9287 pub fn goaway_before_forced_close<L>(&mut self, context: &Context<L>) -> MuxResult
9288 where
9289 L: ListenerHandler + L7ListenerHandler,
9290 {
9291 let result = self.core.goaway_before_forced_close(context);
9292 self.settled(result)
9293 }
9294
9295 /// [`ConnectionH2::start_stream`], settled.
9296 ///
9297 /// Answers the core's `bool` unchanged. The settlement is still required
9298 /// and still belongs HERE: the core reaches
9299 /// `ConnectionH2::graceful_goaway` from its stream-id-exhaustion arm, and
9300 /// the readiness that settlement writes is read by `Mux` before it decides
9301 /// whether this connection gets a `writable` at all.
9302 pub fn start_stream<L>(&mut self, stream: GlobalStreamId, context: &mut Context<L>) -> bool
9303 where
9304 L: ListenerHandler + L7ListenerHandler,
9305 {
9306 let started = self.core.start_stream(stream, context);
9307 self.settle();
9308 started
9309 }
9310
9311 /// [`ConnectionH2::cancel_timed_out_streams`], settled.
9312 ///
9313 /// The core answers nothing and already discards the flood-violation
9314 /// `MuxResult` its `ConnectionH2::enqueue_rst` sweep can raise — see its
9315 /// own comment. That discard is preserved: what this adds is the readiness
9316 /// half of a force-disconnect that discard used to leave completed.
9317 pub fn cancel_timed_out_streams<E, L>(&mut self, context: &mut Context<L>, endpoint: &mut E)
9318 where
9319 E: Endpoint,
9320 L: ListenerHandler + L7ListenerHandler,
9321 {
9322 self.core.cancel_timed_out_streams(
9323 context,
9324 &mut ShellEndpoint::new(endpoint, &self.socket, &self.local_rtt),
9325 );
9326 self.settle();
9327 }
9328
9329 /// [`ConnectionH2::end_stream`], forwarded and drained.
9330 ///
9331 /// No settlement: `ConnectionH2::end_stream` cannot reach
9332 /// `ConnectionH2::force_disconnect`. It is here only because
9333 /// `Connection`'s `forward!` needs one name on both arms.
9334 ///
9335 /// It still has to drain. The core's `end_stream` serialises a
9336 /// RST_STREAM(CANCEL) for a stream the peer abandoned, and that is the one
9337 /// producer this file reaches through a public entry point that settles
9338 /// neither through `H2Shell::settled` nor through `H2Shell::settle`.
9339 pub fn end_stream<L>(&mut self, stream_gid: GlobalStreamId, context: &mut Context<L>)
9340 where
9341 L: ListenerHandler + L7ListenerHandler,
9342 {
9343 self.core.end_stream(stream_gid, context);
9344 self.record_pending_metrics();
9345 }
9346
9347 /// Drive [`ConnectionH2::flush_pending_control_frames`] to a terminal
9348 /// answer, performing the output flush it asks for.
9349 ///
9350 /// The loop is the shell's because the socket is: the core walks its
9351 /// stages, queues their frames behind the ordered output, and stops once
9352 /// with the bytes it cannot move, then is told what the write reported.
9353 /// At most two iterations run, and a stall leaves the loop with
9354 /// [`H2ControlFlushTarget::Stalled`], which is the caller's cue to read
9355 /// the TLS query the stalled write just changed the answer to.
9356 fn drive_control_flush(&mut self) -> ControlFlushOutcome {
9357 let mut target = self.core.flush_pending_control_frames();
9358 loop {
9359 match target {
9360 H2ControlFlushTarget::FlushOutput => {
9361 let stalled = self.flush_output_to_socket();
9362 target = self.core.handle_control_flush(stalled);
9363 }
9364 H2ControlFlushTarget::Proceed => return ControlFlushOutcome::Proceed,
9365 H2ControlFlushTarget::Done(result) => return ControlFlushOutcome::Done(result),
9366 H2ControlFlushTarget::Stalled => return ControlFlushOutcome::Stalled,
9367 }
9368 }
9369 }
9370
9371 fn settled(&mut self, result: MuxResult) -> MuxResult {
9372 let settled = match self.core.poll_force_disconnect() {
9373 None => result,
9374 Some(target) => {
9375 let tls_wants_write = self.tls_wants_write();
9376 self.core
9377 .force_disconnect_after_query(target, tls_wants_write)
9378 }
9379 };
9380 self.record_pending_metrics();
9381 settled
9382 }
9383
9384 /// Record every [`MetricEvent`] the core queued during the step that just
9385 /// ended.
9386 ///
9387 /// The shell half of the boundary: the core states the arithmetic, this
9388 /// writes it into the worker-local `METRICS` aggregator.
9389 ///
9390 /// Three sites, and between them they cover every producer:
9391 ///
9392 /// - [`Self::settled`], so after every `readable`, `writable`,
9393 /// `start_stream`, `cancel_timed_out_streams`, `force_disconnect` and
9394 /// `graceful_goaway`, and after `writable_inner`'s
9395 /// [`Self::drive_control_flush`], whose only caller it is;
9396 /// - [`Self::end_stream`], the one public entry point that reaches a
9397 /// producer and settles through neither;
9398 /// - [`Drop`].
9399 ///
9400 /// The remaining public entry points — `close`, `initiate_close_notify`,
9401 /// `flush_output_buffer`, `has_pending_write`, `has_pending_write_full` —
9402 /// reach no producer but one: they touch the TLS layer, the output queue
9403 /// and readiness, and of the core methods they call
9404 /// (`consume_output_flush`, `ensure_tls_flushed`, `has_pending_write_with`,
9405 /// `output_pending`) only `consume_output_flush` queues a metric, the
9406 /// overhead byte count, which the next [`Self::settled`] records.
9407 ///
9408 /// A step that reached none of the three would still not lose an event:
9409 /// the queue is FIFO, so the next drain records the same values, and
9410 /// `Drop` drains whatever is left. That is what bounds the queue, and what
9411 /// keeps "drain here" a scheduling choice rather than a correctness one.
9412 fn record_pending_metrics(&mut self) {
9413 for event in self.core.drain_metric_events() {
9414 record_metric(event);
9415 }
9416 }
9417
9418 /// [`ConnectionH2::settled`] for a core step whose own answer is not a
9419 /// [`MuxResult`], or is one this layer has always dropped.
9420 ///
9421 /// `ConnectionH2::start_stream` answers a `bool` and
9422 /// `ConnectionH2::cancel_timed_out_streams` answers nothing, yet both
9423 /// reach `ConnectionH2::force_disconnect` — the first through
9424 /// `ConnectionH2::graceful_goaway`, the second through
9425 /// `ConnectionH2::enqueue_rst`'s flood escalation, which it already
9426 /// discards with `let _ =`. The settlement still has to happen HERE rather
9427 /// than at the next entry point, because it writes `readiness` and `Mux`
9428 /// reads `Readiness::filter_interest` between entry points to decide
9429 /// whether to call `readable`/`writable` at all.
9430 fn settle(&mut self) {
9431 let _ = self.settled(MuxResult::Continue);
9432 }
9433
9434 /// Drive one frontend read pass.
9435 ///
9436 /// The core lives in `Self::poll_read_target` and `Self::handle_read`,
9437 /// both `pub`; this function is the caller that sits between them, and its
9438 /// `self.socket.socket_read` is the only socket touch on the whole H2 read
9439 /// path. It lives in the shell impl rather than among the core impls for
9440 /// the reason stated there: the socket touch sits where the socket does,
9441 /// and the frame state machine between the two calls is drivable without
9442 /// one, exactly as `h2_transmit::gather`/`confirm` already bracket the
9443 /// vectored write.
9444 pub fn readable<E, L>(&mut self, context: &mut Context<L>, endpoint: E) -> MuxResult
9445 where
9446 E: Endpoint,
9447 L: ListenerHandler + L7ListenerHandler,
9448 {
9449 if self.core.is_lingering() {
9450 // Entry point: adopt the mux's snapshot, as `poll_read_target`
9451 // would. `graceful_shutdown_deadline_elapsed` reads it, and a
9452 // snapshot frozen at the linger's start would keep a soft stop's
9453 // budget from ever elapsing.
9454 self.core.adopt_now(context.now);
9455 return self.drain_linger(context.now);
9456 }
9457 let result = self.readable_inner(context, endpoint);
9458 self.settled(result)
9459 }
9460
9461 /// Turn a close decided by a writable pass that began after the final
9462 /// GOAWAY ([`ConnectionH2::lingers_instead_of_closing`]) into a lingering
9463 /// close, unless the client hung up meanwhile: queue and
9464 /// flush `close_notify` first (a TLS alert must precede the FIN), then
9465 /// shut the write side down, so the FIN follows everything sent, and
9466 /// read what the client still sends until [`Self::drain_linger`] closes.
9467 /// Any other result is returned unchanged.
9468 fn linger_instead_of_closing(&mut self, result: MuxResult) -> MuxResult {
9469 if !matches!(result, MuxResult::CloseSession) || self.core.frontend_hung_up_while_draining()
9470 {
9471 return result;
9472 }
9473 let (duration, max_bytes) = self.core.linger_budget();
9474 let deadline = match self.core.linger {
9475 Linger::Pending { deadline } => deadline,
9476 _ => match self.core.now.checked_add(duration) {
9477 Some(deadline) => deadline,
9478 None => return MuxResult::CloseSession,
9479 },
9480 };
9481 if self.initiate_close_notify() {
9482 // `close_notify` is pending: the next writable pass flushes it
9483 // and closes again, which lands here once nothing is left. The
9484 // state is `Error` by then, so the pending linger is what keeps
9485 // that pass eligible.
9486 self.core.linger = Linger::Pending { deadline };
9487 return MuxResult::Continue;
9488 }
9489 if let Err(e) = super::shutdown_write(self.socket.socket_ref(), false) {
9490 debug!(
9491 "{} H2 closing without lingering after the final GOAWAY: shutdown failed: {:?}",
9492 log_context!(self.core),
9493 e
9494 );
9495 return MuxResult::CloseSession;
9496 }
9497 debug!(
9498 "{} H2 lingering after the final GOAWAY: draining what the client still sends",
9499 log_context!(self.core)
9500 );
9501 self.core.linger = Linger::Draining {
9502 deadline,
9503 remaining: max_bytes,
9504 };
9505 // Fixed, never re-armed: the deadline bounds the whole drain.
9506 self.core.timeout_deadline = Some(deadline);
9507 self.core.readiness.interest = Ready::READABLE | Ready::HUP | Ready::ERROR;
9508 self.core.readiness.event.remove(Ready::WRITABLE);
9509 // Edge-triggered epoll will not report bytes already queued.
9510 self.core.readiness.event.insert(Ready::READABLE);
9511 MuxResult::Continue
9512 }
9513
9514 /// Read and drop what the client still sends while lingering. Closes on
9515 /// the client's EOF, a socket error, the byte budget, or the deadline;
9516 /// otherwise waits for the next READABLE.
9517 fn drain_linger(&mut self, now: Instant) -> MuxResult {
9518 let Linger::Draining {
9519 deadline,
9520 mut remaining,
9521 } = self.core.linger
9522 else {
9523 unreachable!("drain_linger runs only while lingering");
9524 };
9525 if now >= deadline {
9526 debug!(
9527 "{} H2 lingering close: deadline reached",
9528 log_context!(self.core)
9529 );
9530 return MuxResult::CloseSession;
9531 }
9532 let result = match drain_discard(self.socket.socket_ref(), &mut remaining, |size| {
9533 record_metric(MetricEvent::FrontendBytesIn(size as i64));
9534 }) {
9535 LingerRead::Wait => {
9536 self.core.readiness.event.remove(Ready::READABLE);
9537 MuxResult::Continue
9538 }
9539 LingerRead::Close => {
9540 debug!(
9541 "{} H2 lingering close: EOF, error or {} bytes left of the budget",
9542 log_context!(self.core),
9543 remaining
9544 );
9545 MuxResult::CloseSession
9546 }
9547 };
9548 self.core.linger = Linger::Draining {
9549 deadline,
9550 remaining,
9551 };
9552 result
9553 }
9554
9555 fn readable_inner<E, L>(&mut self, context: &mut Context<L>, mut endpoint: E) -> MuxResult
9556 where
9557 E: Endpoint,
9558 L: ListenerHandler + L7ListenerHandler,
9559 {
9560 match self.core.poll_read_target(
9561 context,
9562 &mut ShellEndpoint::new(&mut endpoint, &self.socket, &self.local_rtt),
9563 ) {
9564 H2ReadTarget::Done(result) => result,
9565 H2ReadTarget::Skip(stream_id) => self.core.handle_read(
9566 context,
9567 ShellEndpoint::new(&mut endpoint, &self.socket, &self.local_rtt),
9568 stream_id,
9569 H2ReadOutcome::Skipped,
9570 ),
9571 H2ReadTarget::Fill { stream_id, amount } => {
9572 let space = self.core.read_space(context, stream_id, amount);
9573 let (size, status) = self.socket.socket_read(space);
9574 self.core.handle_read(
9575 context,
9576 ShellEndpoint::new(&mut endpoint, &self.socket, &self.local_rtt),
9577 stream_id,
9578 H2ReadOutcome::Filled {
9579 amount,
9580 size,
9581 status,
9582 },
9583 )
9584 }
9585 }
9586 }
9587
9588 /// Drive one frontend write pass.
9589 ///
9590 /// The core lives in `Self::flush_pending_control_frames`,
9591 /// `Self::dispatch_writable_state` and
9592 /// `Self::dispatch_writable_state_after_flush`; this function is the
9593 /// caller that sits between them, and every TLS seam call this function's
9594 /// own body used to make inline is here. It lives in the shell impl rather
9595 /// than among the core impls for the reason stated there: the socket
9596 /// touches sit where the socket does, and the `(H2State, Position)`
9597 /// dispatch between them is drivable without one.
9598 ///
9599 /// The pass performs the query / flush / query triple `h2_close` names
9600 /// TWICE, and the second time only for one arm:
9601 ///
9602 /// 1. the preamble — read `Self::tls_wants_write`, flush if it says yes.
9603 /// It pushes bytes for every state, which is why it is unconditional
9604 /// rather than owned by a close arm.
9605 /// 2. a FRESH read of the same query, which is that preamble's post-flush
9606 /// answer and the pre-flush question both close arms decide on. It is
9607 /// handed to `Self::dispatch_writable_state` as an input.
9608 /// 3. for `H2WritableStateTarget::Flush` alone — the `H2State::GoAway`
9609 /// arm — `Self::flush_tls_records` again, a third read, and
9610 /// `Self::dispatch_writable_state_after_flush`.
9611 ///
9612 /// Each read is a separate `let`, and the third shadows the second inside
9613 /// the `Flush` arm only. That is the whole guard against the confusion
9614 /// `h2_close::TlsFlushPhase` exists to prevent: "does rustls hold records"
9615 /// and "did the kernel take them" are two questions, the second cannot be
9616 /// read off the first, and a core that reused one answer for both would
9617 /// close a connection with records pending.
9618 ///
9619 /// The tick count is load-bearing: the flush and the re-query happen
9620 /// within THIS call and the pass does not return `Continue` to be called
9621 /// again, which `a_flush_that_succeeds_closes_within_one_writable_call`
9622 /// pins. See `h2_close`'s module doc for why.
9623 pub fn writable<E, L>(&mut self, context: &mut Context<L>, endpoint: E) -> MuxResult
9624 where
9625 E: Endpoint,
9626 L: ListenerHandler + L7ListenerHandler,
9627 {
9628 if self.core.is_lingering() {
9629 // Entry point: adopt the mux's snapshot, as `writable_inner` does.
9630 self.core.adopt_now(context.now);
9631 // The write side is shut down: nothing is left to write.
9632 self.core.readiness.interest.remove(Ready::WRITABLE);
9633 self.core.readiness.event.remove(Ready::WRITABLE);
9634 return MuxResult::Continue;
9635 }
9636 // Asked before the pass: the close it decides moves the state from
9637 // `GoAway` to `Error` (`ConnectionH2::force_disconnect`).
9638 let after_final_goaway = self.core.lingers_instead_of_closing();
9639 let result = self.writable_inner(context, endpoint);
9640 if self.write_failed {
9641 // The client is gone or its socket can never be flushed: close
9642 // at once, as an H1 frontend does (`ConnectionH1::writable`).
9643 debug!(
9644 "{} H2 closing the frontend after a failed write",
9645 log_context!(self.core)
9646 );
9647 return MuxResult::CloseSession;
9648 }
9649 let result = self.settled(result);
9650 if after_final_goaway {
9651 self.linger_instead_of_closing(result)
9652 } else {
9653 result
9654 }
9655 }
9656
9657 fn writable_inner<E, L>(&mut self, context: &mut Context<L>, endpoint: E) -> MuxResult
9658 where
9659 E: Endpoint,
9660 L: ListenerHandler + L7ListenerHandler,
9661 {
9662 // Entry point: adopt the mux's snapshot for this pass.
9663 self.core.adopt_now(context.now);
9664 // Only this pass's writes may withhold its TLS re-arm.
9665 self.core.kernel_refused_write = false;
9666 self.core.prune_inactive_streams_while_closing(context);
9667
9668 match self.drive_control_flush() {
9669 ControlFlushOutcome::Done(result) => return result,
9670 // The stalled-drain tail the stages used to run inline. The
9671 // query belongs here and not there: `flush_output_to_socket` is what
9672 // changed the answer, so it has to be read after the stall, and
9673 // this is the layer that owns the socket.
9674 ControlFlushOutcome::Stalled => {
9675 let tls_wants_write = self.tls_wants_write();
9676 self.core.ensure_tls_flushed(tls_wants_write);
9677 return MuxResult::Continue;
9678 }
9679 ControlFlushOutcome::Proceed => {}
9680 }
9681
9682 // Flush any pending TLS records before state-specific processing.
9683 // This ensures response DATA frames that were accepted by rustls
9684 // (via socket_write_vectored in write_streams) are pushed to the
9685 // TCP socket even when the connection is in GoAway or Error state.
9686 // Without this, the state-specific handlers may call force_disconnect()
9687 // before the response data reaches the kernel's TCP send buffer.
9688 if self.tls_wants_write() {
9689 self.flush_tls_records();
9690 }
9691
9692 // A fresh query, not the binding above: this one asks whether the
9693 // flush between them landed. It is read here rather than inside the
9694 // two close arms that consult it because those arms no longer touch
9695 // the socket, and it is read ONCE because only one of them can run.
9696 let tls_wants_write = self.tls_wants_write();
9697 match self.core.dispatch_writable_state(tls_wants_write) {
9698 H2WritableStateTarget::Done(result) => result,
9699 // The `H2State::GoAway` arm's own flush, the second this pass
9700 // attempts, and the only *action* the close decision cannot take
9701 // for itself.
9702 H2WritableStateTarget::Flush => {
9703 self.flush_tls_records();
9704 // Again a fresh query, and again a different question from
9705 // the one that produced `Flush`: `flush_tls_records`'s
9706 // `(size, status)` is discarded, so this read is the only way
9707 // the pass learns whether the records reached the kernel.
9708 let tls_wants_write = self.tls_wants_write();
9709 self.core
9710 .dispatch_writable_state_after_flush(tls_wants_write)
9711 }
9712 H2WritableStateTarget::WriteStreams => self.write_streams(context, endpoint),
9713 }
9714 }
9715
9716 /// Write application data (request/response bodies, headers) across all
9717 /// active streams, respecting priority ordering and flow control.
9718 ///
9719 /// This is the main data-plane write path: it resumes any partially-written
9720 /// stream, prepares new frames via the H2 block converter, flushes them to
9721 /// the socket, and recycles completed streams.
9722 ///
9723 /// The core lives in [`ConnectionH2::poll_write_target`] and [`ConnectionH2::handle_write`],
9724 /// both `pub`; this function is the caller that sits between them, and its
9725 /// `self.socket.socket_write_vectored` is the only place a stream's bytes
9726 /// reach the socket. It lives in the shell impl rather than among the core
9727 /// impls for the reason stated there, exactly as `readable()` keeps
9728 /// `socket_read` outside [`ConnectionH2::poll_read_target`].
9729 ///
9730 /// The pass's last step is a socket touch too, and for the same reason it
9731 /// sits here: [`H2WriteTarget::Finalize`] is answered by reading
9732 /// [`Self::tls_wants_write`], driving the [`H2FinalizeTarget`]
9733 /// [`ConnectionH2::finalize_write`] answers — performing
9734 /// [`Self::flush_tls_records`] for [`H2FinalizeTarget::Flush`] and not for
9735 /// [`H2FinalizeTarget::SkipFlush`] — and reading the query again for
9736 /// [`ConnectionH2::finalize_write_after_flush`]. That is the query / flush / query
9737 /// triple `h2_close::TlsFlushPhase` names, and the three TLS seams it
9738 /// reaches are what a byte-in / byte-out core receives as inputs instead
9739 /// of asking for.
9740 ///
9741 /// Unlike `readable()` this is a LOOP and not a match: one read pass
9742 /// performs one `socket_read`, while one write pass performs an unbounded
9743 /// number of vectored writes — the pre-image's `while !kawa.out.is_empty()`
9744 /// nested inside its walk of the scheduler's order. Every iteration here is
9745 /// one of those rounds.
9746 ///
9747 /// The `Vec<IoSlice<'static>>` came here WITH the loop, and is only ever
9748 /// filled inside this one private function, on purpose. It is the shell's
9749 /// private `io_slices` field rather than a local so that a transmitting
9750 /// pass reuses its capacity instead of allocating it: the local cost one
9751 /// heap allocation per pass, at least one per response.
9752 /// [`h2_transmit::gather`] is a `pub unsafe fn` that hands back
9753 /// descriptors carrying a lifetime they do
9754 /// not have, and [`h2_transmit::confirm`] must discharge them before the
9755 /// consume that may relocate `kawa.storage`; bracketing the two around the
9756 /// write in three adjacent statements keeps that window exactly as wide as
9757 /// the pre-image's loop body kept it, and stops it from spanning the
9758 /// poll/handle boundary — now a `pub` one, which is why the bracket's
9759 /// locality is load-bearing rather than tidy. The `unsafe { … }` below
9760 /// wraps the `gather` call alone; see the shell impl's own note.
9761 ///
9762 /// The [`converter::H2BlockConverter`] is scoped to a single
9763 /// `kawa.prepare` call rather than to the whole per-stream loop, so the
9764 /// `&mut self.core.hpack` borrow it takes for the connection's HPACK encoder
9765 /// never spans the loop. Every `&self` / `&mut self` method is therefore
9766 /// callable inside the loop body; what remains deferred to after it —
9767 /// RST accounting and stream retirement — is deferred for its own
9768 /// ordering reasons, documented at each site.
9769 /// [`converter::H2ConverterPass`] carries the scratch buffers and the
9770 /// RFC 7541 §6.3 size-update signal from one stream's `prepare` to the
9771 /// next by moving them, so the narrower scope costs no copy.
9772 fn write_streams<E, L>(&mut self, context: &mut Context<L>, mut endpoint: E) -> MuxResult
9773 where
9774 E: Endpoint,
9775 L: ListenerHandler + L7ListenerHandler,
9776 {
9777 // No `arm_timeout()` here. `writable()` reaches this function on every
9778 // proxying-state pass, including one whose only output was the PING or
9779 // SETTINGS acknowledgement `flush_pending_control_frames` drained a few
9780 // statements earlier, so arming at pass entry renewed the session for a
9781 // peer that had sent nothing but keepalives. The write side's share of
9782 // LIFECYCLE §9 invariant 9 now lives in `ConnectionH2::handle_write`,
9783 // where the pass knows a real stream's bytes reached the socket.
9784 // Pre-compute byte totals for proportional overhead distribution.
9785 let mut pass = H2WritePass::new(self.core.compute_stream_byte_totals(context));
9786
9787 loop {
9788 match self.core.poll_write_target(
9789 context,
9790 &mut ShellEndpoint::new(&mut endpoint, &self.socket, &self.local_rtt),
9791 &mut pass,
9792 ) {
9793 H2WriteTarget::Done(result) => return result,
9794 H2WriteTarget::Finalize {
9795 socket_write,
9796 bytes_written,
9797 } => {
9798 let tls_wants_write = self.tls_wants_write();
9799 match self.core.finalize_write(
9800 tls_wants_write,
9801 socket_write,
9802 bytes_written,
9803 context,
9804 ) {
9805 H2FinalizeTarget::Done(result) => return result,
9806 // The only *action* of the three TLS seams, and the
9807 // one step the core cannot take for itself.
9808 H2FinalizeTarget::Flush => {
9809 self.flush_tls_records();
9810 }
9811 // The vectored writes above already attempted this
9812 // pass's flush; a second empty-buffer write would be a
9813 // syscall for nothing.
9814 H2FinalizeTarget::SkipFlush => {}
9815 }
9816 // A fresh query, not the binding above: this one asks
9817 // whether the step between them landed.
9818 let tls_wants_write = self.tls_wants_write();
9819 return self.core.finalize_write_after_flush(tls_wants_write);
9820 }
9821 H2WriteTarget::Transmit { stream_id } => {
9822 // Gather / write / measure / confirm. The gather borrows
9823 // the queued output and `kawa.storage`, and hands back
9824 // descriptors with an extended lifetime; `confirm`
9825 // discharges that obligation before the consume.
9826 //
9827 // SAFETY: neither the output queue nor `kawa` is dropped
9828 // or mutated between this call and the `confirm` below,
9829 // and that `confirm` clears `self.io_slices` before the
9830 // `Kawa::consume` which may relocate `kawa.storage`. The
9831 // only things entered while the descriptors are live are
9832 // the vectored write, which reborrows them for the
9833 // duration of the call and cannot retain them, and
9834 // `frame_tail`, which only reads them.
9835 let (prefix, offered) = unsafe {
9836 self.core
9837 .gather_transmit(context, stream_id, &mut self.io_slices)
9838 };
9839 let (size, status) = self.socket.socket_write_vectored(&self.io_slices);
9840 self.record_write(status);
9841 debug_assert!(
9842 size <= offered,
9843 "the socket reported {size} bytes written for an offer of {offered}"
9844 );
9845 let stream_size = size.saturating_sub(prefix);
9846 // A write that took everything cut nothing: the walk is
9847 // only for a partial write.
9848 let tail = if size < offered {
9849 h2_transmit::frame_tail(
9850 &self.io_slices[usize::from(prefix > 0)..],
9851 stream_size,
9852 )
9853 } else {
9854 0
9855 };
9856 let kawa = self.core.write_buffer(context, stream_id);
9857 h2_transmit::confirm(kawa, &mut self.io_slices, stream_size);
9858 self.core.handle_write(
9859 context,
9860 stream_id,
9861 H2Written {
9862 size,
9863 from_output: size - stream_size,
9864 tail,
9865 },
9866 status,
9867 &mut pass,
9868 );
9869 }
9870 }
9871 }
9872 }
9873}
9874
9875#[cfg(test)]
9876pub(super) mod tests {
9877 use std::{cell::RefCell, collections::HashMap, rc::Rc};
9878
9879 use super::*;
9880 use crate::{
9881 pool::Pool,
9882 protocol::{
9883 kawa_h1::editor::HttpContext,
9884 mux::{
9885 BackendChange, BackendDelta, Connection,
9886 buffer_source::PoolBufferSource,
9887 connection::EndpointClient,
9888 router::Router,
9889 test_support::{TestListener, connected_socket, test_context},
9890 },
9891 },
9892 socket::SessionTcpStream,
9893 };
9894
9895 // ── H2FloodDetector / H2FloodViolation ─────────────────────────────
9896 //
9897 // H2FloodDetector's own unit tests (threshold trips, half-decay,
9898 // lifetime ceilings, metric-key uniqueness) moved to
9899 // `h2_flood_detector.rs`'s `#[cfg(test)] mod tests` alongside the type —
9900 // its fields are private to that module now, matching `HpackState`,
9901 // `H2FlowControl` and `H2StreamTable`. The old test comparing
9902 // default-constructed vs. explicitly-constructed detectors did not
9903 // move: it compared `H2FloodDetector::default()` against
9904 // `H2FloodDetector::new(...)`, and this extraction removed `impl Default
9905 // for H2FloodDetector` entirely (LIFECYCLE.md invariant 20's last
9906 // self-sampling-clock exception), so there is no second constructor left
9907 // for it to compare against.
9908
9909 // ── Prioriser / scheduler ───────────────────────────────────────────
9910 //
9911 // `Prioriser`'s own unit tests (defaults, the RFC 9218 urgency clamp,
9912 // the `MAX_PRIORITIES` cap, the standalone-PRIORITY acceptance filter,
9913 // the same-urgency incremental rotation) moved to `h2_scheduler.rs`'s
9914 // `#[cfg(test)] mod tests` alongside the type — its fields are private
9915 // to that module now, matching `HpackState`, `H2FlowControl`,
9916 // `H2StreamTable` and `H2FloodDetector`. The pass-scoped scheduling
9917 // tests that used to re-implement `ready_incremental_by_urgency`'s
9918 // arithmetic inline in the test body moved there too, and now drive
9919 // `ReadyIncrementalCensus`'s production methods instead of a copy of
9920 // them.
9921
9922 // ── H2FlowControl ───────────────────────────────────────────────────
9923 //
9924 // H2FlowControl's own unit tests (initial state, coalescing, saturation,
9925 // negative window, the ordering-determinism regression test) moved to
9926 // `h2_flow_control.rs`'s `#[cfg(test)] mod tests` alongside the type —
9927 // its fields are private to that module now, matching `HpackState`.
9928
9929 // ── H2FloodConfig ───────────────────────────────────────────────────
9930 //
9931 // Moved to `h2_flood_detector.rs`'s `#[cfg(test)] mod tests` alongside
9932 // `H2FloodConfig`, `H2FloodViolation` and `H2FloodDetector`.
9933
9934 // ── distribute_overhead ─────────────────────────────────────────────
9935
9936 #[test]
9937 fn test_distribute_overhead_proportional() {
9938 let mut metrics = SessionMetrics::new(None);
9939 let mut overhead_bin = 1000;
9940 let mut overhead_bout = 500;
9941
9942 // Stream transferred 60% of total bytes (not last stream)
9943 distribute_overhead(
9944 &mut metrics,
9945 &mut overhead_bin,
9946 &mut overhead_bout,
9947 (600, 300), // stream_bytes
9948 (1000, 500), // total_bytes
9949 2, // active_streams
9950 false, // is_last_stream
9951 );
9952
9953 assert_eq!(metrics.bin, 600); // 60% of 1000
9954 assert_eq!(metrics.bout, 300); // 60% of 500
9955 assert_eq!(overhead_bin, 400); // 1000 - 600
9956 assert_eq!(overhead_bout, 200); // 500 - 300
9957 }
9958
9959 #[test]
9960 fn test_distribute_overhead_even_split_when_no_bytes() {
9961 let mut metrics = SessionMetrics::new(None);
9962 let mut overhead_bin = 100;
9963 let mut overhead_bout = 200;
9964
9965 // No bytes transferred -> even distribution (not last stream)
9966 distribute_overhead(
9967 &mut metrics,
9968 &mut overhead_bin,
9969 &mut overhead_bout,
9970 (0, 0), // stream_bytes
9971 (0, 0), // total_bytes
9972 4, // active_streams
9973 false, // is_last_stream
9974 );
9975
9976 assert_eq!(metrics.bin, 25); // 100 / 4
9977 assert_eq!(metrics.bout, 50); // 200 / 4
9978 assert_eq!(overhead_bin, 75);
9979 assert_eq!(overhead_bout, 150);
9980 }
9981
9982 #[test]
9983 fn test_distribute_overhead_clamps_to_remaining() {
9984 let mut metrics = SessionMetrics::new(None);
9985 let mut overhead_bin = 10;
9986 let mut overhead_bout = 10;
9987
9988 // Stream claims 100% of bytes but overhead is small (last stream)
9989 distribute_overhead(
9990 &mut metrics,
9991 &mut overhead_bin,
9992 &mut overhead_bout,
9993 (1000, 1000), // stream_bytes
9994 (1000, 1000), // total_bytes
9995 1, // active_streams
9996 true, // is_last_stream
9997 );
9998
9999 assert_eq!(metrics.bin, 10);
10000 assert_eq!(metrics.bout, 10);
10001 assert_eq!(overhead_bin, 0);
10002 assert_eq!(overhead_bout, 0);
10003 }
10004
10005 #[test]
10006 fn test_distribute_overhead_zero_active_streams() {
10007 let mut metrics = SessionMetrics::new(None);
10008 let mut overhead_bin = 100;
10009 let mut overhead_bout = 100;
10010
10011 // 0 active streams (edge case) — last stream gets all remainder
10012 distribute_overhead(
10013 &mut metrics,
10014 &mut overhead_bin,
10015 &mut overhead_bout,
10016 (0, 0),
10017 (0, 0),
10018 0,
10019 true,
10020 );
10021
10022 assert_eq!(metrics.bin, 100); // last stream gets all remaining
10023 assert_eq!(metrics.bout, 100);
10024 assert_eq!(overhead_bin, 0);
10025 assert_eq!(overhead_bout, 0);
10026 }
10027
10028 #[test]
10029 fn test_distribute_overhead_last_stream_gets_remainder() {
10030 let mut metrics1 = SessionMetrics::new(None);
10031 let mut metrics2 = SessionMetrics::new(None);
10032 let mut overhead_bin = 120;
10033 let mut overhead_bout = 120;
10034
10035 // First stream (not last): gets proportional share
10036 distribute_overhead(
10037 &mut metrics1,
10038 &mut overhead_bin,
10039 &mut overhead_bout,
10040 (100, 100), // stream_bytes
10041 (300, 300), // total_bytes
10042 3, // active_streams
10043 false, // is_last_stream
10044 );
10045
10046 let remaining_bin = overhead_bin;
10047 let remaining_bout = overhead_bout;
10048
10049 // Last stream: gets ALL remaining overhead (no rounding loss)
10050 distribute_overhead(
10051 &mut metrics2,
10052 &mut overhead_bin,
10053 &mut overhead_bout,
10054 (100, 100), // stream_bytes
10055 (300, 300), // total_bytes
10056 3, // active_streams
10057 true, // is_last_stream
10058 );
10059
10060 assert_eq!(metrics2.bin, remaining_bin);
10061 assert_eq!(metrics2.bout, remaining_bout);
10062 assert_eq!(overhead_bin, 0, "no remainder bytes should be lost");
10063 assert_eq!(overhead_bout, 0, "no remainder bytes should be lost");
10064 }
10065
10066 // ── H2FlowControl (additional edge cases) ─────────────────────────
10067
10068 #[test]
10069 fn test_flow_control_queue_window_update_cap() {
10070 // Verify DEFAULT_MAX_PENDING_WINDOW_UPDATES reflects 1 + 4*MAX_CONCURRENT_STREAMS
10071 assert_eq!(DEFAULT_MAX_PENDING_WINDOW_UPDATES, 1 + 100 * 4);
10072
10073 // Simulate queue reaching capacity
10074 let cap = DEFAULT_MAX_PENDING_WINDOW_UPDATES;
10075 let mut updates: HashMap<u32, u32> = HashMap::new();
10076 for i in 0..cap as u32 {
10077 updates.insert(i, 1000);
10078 }
10079 assert_eq!(updates.len(), cap);
10080
10081 // A new stream ID beyond capacity should be rejected
10082 let next_stream = cap as u32;
10083 let at_cap = updates.len() >= cap;
10084 assert!(at_cap);
10085 assert!(!updates.contains_key(&next_stream));
10086
10087 // Verify custom max_concurrent_streams produces proportional cap
10088 let custom_cap = 1 + 500_usize * 4;
10089 assert_eq!(custom_cap, 2001);
10090 }
10091
10092 #[test]
10093 fn test_h2_connection_config_defaults() {
10094 let config = H2ConnectionConfig::default();
10095 assert_eq!(config.initial_connection_window, ENLARGED_CONNECTION_WINDOW);
10096 assert_eq!(
10097 config.max_concurrent_streams,
10098 DEFAULT_MAX_CONCURRENT_STREAMS
10099 );
10100 assert_eq!(config.stream_shrink_ratio, 2);
10101 }
10102
10103 #[test]
10104 fn test_h2_connection_config_clamp_window_lower_bound() {
10105 // Below minimum: clamped to DEFAULT_INITIAL_WINDOW_SIZE (65535)
10106 let config = H2ConnectionConfig::new(100, 100, 2);
10107 assert_eq!(
10108 config.initial_connection_window,
10109 DEFAULT_INITIAL_WINDOW_SIZE
10110 );
10111 }
10112
10113 #[test]
10114 fn test_h2_connection_config_clamp_window_upper_bound() {
10115 // Above maximum: clamped to FLOW_CONTROL_MAX_WINDOW (2^31-1)
10116 let config = H2ConnectionConfig::new(u32::MAX, 100, 2);
10117 assert_eq!(config.initial_connection_window, FLOW_CONTROL_MAX_WINDOW);
10118 }
10119
10120 #[test]
10121 fn test_h2_connection_config_clamp_window_exact_minimum() {
10122 // Exactly minimum: no clamping, no zero-increment WINDOW_UPDATE risk
10123 let config = H2ConnectionConfig::new(DEFAULT_INITIAL_WINDOW_SIZE, 100, 2);
10124 assert_eq!(
10125 config.initial_connection_window,
10126 DEFAULT_INITIAL_WINDOW_SIZE
10127 );
10128 // Increment to send would be 0 — the code guards this with `if increment > 0`
10129 let increment = config
10130 .initial_connection_window
10131 .saturating_sub(DEFAULT_INITIAL_WINDOW_SIZE);
10132 assert_eq!(increment, 0);
10133 }
10134
10135 #[test]
10136 fn test_h2_connection_config_clamp_shrink_ratio() {
10137 // Below minimum: clamped to 2 (1 would defeat recycling)
10138 let config = H2ConnectionConfig::new(ENLARGED_CONNECTION_WINDOW, 100, 0);
10139 assert_eq!(config.stream_shrink_ratio, 2);
10140 let config = H2ConnectionConfig::new(ENLARGED_CONNECTION_WINDOW, 100, 1);
10141 assert_eq!(config.stream_shrink_ratio, 2);
10142 }
10143
10144 #[test]
10145 fn test_h2_connection_config_clamp_concurrent_streams() {
10146 let config = H2ConnectionConfig::new(ENLARGED_CONNECTION_WINDOW, 0, 2);
10147 assert_eq!(config.max_concurrent_streams, 1);
10148 }
10149
10150 #[test]
10151 fn test_h2_connection_config_from_optional_uses_defaults() {
10152 let config = H2ConnectionConfig::from_optional(None, None, None);
10153 let defaults = H2ConnectionConfig::default();
10154 assert_eq!(config, defaults);
10155 }
10156
10157 #[test]
10158 fn test_h2_connection_config_from_optional_overrides() {
10159 let config = H2ConnectionConfig::from_optional(Some(2_000_000), Some(500), Some(4));
10160 assert_eq!(config.initial_connection_window, 2_000_000);
10161 assert_eq!(config.max_concurrent_streams, 500);
10162 assert_eq!(config.stream_shrink_ratio, 4);
10163 }
10164
10165 // The old settings-change-negative-window test was removed here, not
10166 // moved: its docstring premise was wrong. `update_initial_window_size`
10167 // (below) only ever adjusts *per-stream* windows on a
10168 // SETTINGS_INITIAL_WINDOW_SIZE change, never the connection-level
10169 // `flow_control.window` that test constructed and mutated directly — so
10170 // it never exercised a real code path. `h2_flow_control.rs`'s test
10171 // `consume_send_window_saturates_at_i32_min_without_wrapping` covers a
10172 // genuinely different, previously-untested boundary of the same
10173 // `consume_send_window` API (the `saturating_sub` floor at `i32::MIN`);
10174 // it does not restore the deleted test's coverage, because the deleted
10175 // test covered nothing real.
10176
10177 #[test]
10178 fn test_flow_control_coalesce_saturates_at_max_increment() {
10179 let max_increment = i32::MAX as u32;
10180 let mut updates: HashMap<u32, u32> = HashMap::new();
10181
10182 // Insert at max and try to coalesce more
10183 updates.insert(1, max_increment);
10184 if let Some(existing) = updates.get_mut(&1) {
10185 *existing = existing.saturating_add(1000).min(max_increment);
10186 }
10187 assert_eq!(*updates.get(&1).unwrap(), max_increment);
10188 }
10189
10190 // ── H2FloodConfig (additional) ───────────────────────────────────
10191 //
10192 // Moved to `h2_flood_detector.rs`'s `#[cfg(test)] mod tests`
10193 // (`test_flood_config_default_matches_constants`, `test_flood_config_equality`).
10194
10195 // ── distribute_overhead (additional edge cases) ───────────────────
10196
10197 #[test]
10198 fn test_distribute_overhead_asymmetric_in_out() {
10199 let mut metrics = SessionMetrics::new(None);
10200 let mut overhead_bin = 1000;
10201 let mut overhead_bout = 1000;
10202
10203 // Stream transferred 100% inbound, 0% outbound (not last stream)
10204 distribute_overhead(
10205 &mut metrics,
10206 &mut overhead_bin,
10207 &mut overhead_bout,
10208 (500, 0), // stream_bytes
10209 (500, 100), // total_bytes
10210 2, // active_streams
10211 false, // is_last_stream
10212 );
10213
10214 assert_eq!(metrics.bin, 1000); // 100% of inbound overhead
10215 assert_eq!(metrics.bout, 0); // 0% of outbound overhead
10216 assert_eq!(overhead_bin, 0);
10217 assert_eq!(overhead_bout, 1000);
10218 }
10219
10220 #[test]
10221 fn test_distribute_overhead_many_streams_accumulate() {
10222 let mut metrics = SessionMetrics::new(None);
10223 let mut overhead_bin = 120;
10224 let mut overhead_bout = 120;
10225
10226 // Three equal streams, each calling distribute_overhead.
10227 // With is_last_stream on the third call, the last stream gets all
10228 // remaining overhead, so no rounding loss occurs.
10229 // call 1: 120 * 100/300 = 40 -> remaining 80
10230 // call 2: 80 * 100/300 = 26 -> remaining 54
10231 // call 3: last stream gets all remaining = 54
10232 // Total distributed: 40 + 26 + 54 = 120 (no loss)
10233 for i in 0..3 {
10234 distribute_overhead(
10235 &mut metrics,
10236 &mut overhead_bin,
10237 &mut overhead_bout,
10238 (100, 100), // stream_bytes
10239 (300, 300), // total_bytes
10240 3, // active_streams
10241 i == 2, // is_last_stream on final call
10242 );
10243 }
10244
10245 assert_eq!(metrics.bin, 120);
10246 assert_eq!(metrics.bout, 120);
10247 // No rounding residual — last stream absorbed the remainder
10248 assert_eq!(overhead_bin, 0);
10249 assert_eq!(overhead_bout, 0);
10250 }
10251
10252 // ── Hex chunk formatting ────────────────────────────────────────────
10253
10254 /// Verify that the Vec<u8> + write!() hex formatting used in
10255 /// handle_data_frame produces output identical to format!("{:x}").
10256 #[test]
10257 fn test_hex_chunk_length_formatting() {
10258 use std::io::Write as _;
10259
10260 let cases: &[(usize, &[u8])] = &[
10261 (1, b"1"),
10262 (15, b"f"),
10263 (16, b"10"),
10264 (255, b"ff"),
10265 (256, b"100"),
10266 (4096, b"1000"),
10267 (65535, b"ffff"),
10268 (65536, b"10000"),
10269 ];
10270
10271 for &(payload_len, expected) in cases {
10272 let mut buf = Vec::with_capacity(16);
10273 let _ = write!(buf, "{payload_len:x}");
10274 assert_eq!(
10275 buf, expected,
10276 "hex formatting mismatch for payload_len={payload_len}"
10277 );
10278 }
10279
10280 // usize::MAX tested separately to avoid temporary lifetime issue
10281 let max_expected = format!("{:x}", usize::MAX);
10282 let mut buf = Vec::with_capacity(16);
10283 let _ = write!(buf, "{:x}", usize::MAX);
10284 assert_eq!(buf, max_expected.as_bytes());
10285 }
10286
10287 // ── Stream-ID allocation / exhaustion ──────────────────────────────────
10288
10289 /// A fresh client connection starts with `last_stream_id == 0`. The first
10290 /// call MUST issue stream `1` (odd, RFC 9113 §5.1.1) and advance the
10291 /// watermark to `2`.
10292 #[test]
10293 fn test_next_stream_id_client_first_allocation() {
10294 let (issued, next) = next_stream_id(0, true).expect("fresh client must allocate");
10295 assert_eq!(issued, 1);
10296 assert_eq!(next, 2);
10297 }
10298
10299 /// Client allocation yields strictly increasing odd identifiers
10300 /// (1, 3, 5, ...) as required by RFC 9113 §5.1.1.
10301 #[test]
10302 fn test_next_stream_id_client_sequence_is_odd_and_monotonic() {
10303 let mut last = 0u32;
10304 let mut issued_ids = Vec::with_capacity(8);
10305 for _ in 0..8 {
10306 let (id, next) = next_stream_id(last, true).expect("unexhausted");
10307 assert_eq!(id & 1, 1, "client stream ids must be odd (RFC 9113 §5.1.1)");
10308 assert!(issued_ids.last().is_none_or(|prev: &u32| id > *prev));
10309 issued_ids.push(id);
10310 last = next;
10311 }
10312 assert_eq!(issued_ids, vec![1, 3, 5, 7, 9, 11, 13, 15]);
10313 }
10314
10315 /// Server-side allocation yields even identifiers. The helper
10316 /// convention is `watermark - 2` for server, `watermark - 1` for client,
10317 /// so both sides share the same monotonically-increasing even watermark.
10318 /// Sōzu never server-pushes, but the helper must be symmetric so push
10319 /// could be enabled without a regression.
10320 #[test]
10321 fn test_next_stream_id_server_is_even() {
10322 // `last = 2` means the most recent allocation advanced the watermark
10323 // to 2; server then issues `2 - 2 = 0`. This is an artefact of the
10324 // shared watermark and only matters in tests — server never uses it.
10325 let (issued, next) = next_stream_id(2, false).expect("server allocation");
10326 assert_eq!(issued & 1, 0, "server stream ids must be even");
10327 assert_eq!(next, 4);
10328 assert_eq!(issued, 2);
10329
10330 let (issued, next) = next_stream_id(next, false).expect("second slot");
10331 assert_eq!(issued, 4);
10332 assert_eq!(issued & 1, 0);
10333 assert_eq!(next, 6);
10334 }
10335
10336 /// The last client-issuable odd stream ID is `STREAM_ID_MAX = 0x7FFF_FFFF`.
10337 /// To issue it the watermark must advance to `STREAM_ID_MAX + 1 = 2³¹`;
10338 /// the caller therefore supplies `last = STREAM_ID_MAX - 1 = 0x7FFF_FFFE`.
10339 /// That call MUST succeed and return the max ID; the post-call watermark
10340 /// sits at `2³¹`, which is the sentinel that makes the next call fail.
10341 #[test]
10342 fn test_next_stream_id_client_final_slot_allocates() {
10343 let last = STREAM_ID_MAX - 1;
10344 let (issued, next) = next_stream_id(last, true).expect("final slot still allocates");
10345 assert_eq!(issued, STREAM_ID_MAX);
10346 assert_eq!(next, STREAM_ID_MAX + 1);
10347 // And the very next call MUST refuse rather than wrap.
10348 assert!(next_stream_id(next, true).is_none());
10349 }
10350
10351 /// Exhaustion case: once the client has issued stream ID `STREAM_ID_MAX`,
10352 /// the watermark sits at `STREAM_ID_MAX + 1`. The next request MUST return
10353 /// `None` — without this guard the helper would issue `STREAM_ID_MAX + 2`
10354 /// (wrapped down to an even id), which would (a) use the reserved
10355 /// high bit and (b) violate the odd-parity invariant for client streams.
10356 #[test]
10357 fn test_next_stream_id_client_exhausted_returns_none() {
10358 let last = STREAM_ID_MAX + 1;
10359 assert!(next_stream_id(last, true).is_none());
10360 }
10361
10362 /// Exhaustion via `checked_add` saturation: defence in depth in case a
10363 /// caller jumps `last_stream_id` close to `u32::MAX`. The helper must
10364 /// not panic nor overflow — it must return `None`.
10365 #[test]
10366 fn test_next_stream_id_saturates_near_u32_max() {
10367 assert!(next_stream_id(u32::MAX, true).is_none());
10368 assert!(next_stream_id(u32::MAX - 1, true).is_none());
10369 }
10370
10371 /// Server-side exhaustion: same guard, even-parity identifier space.
10372 #[test]
10373 fn test_next_stream_id_server_exhausted_returns_none() {
10374 let last = STREAM_ID_MAX + 1;
10375 assert!(next_stream_id(last, false).is_none());
10376 }
10377
10378 /// Regression guard: the helper must never issue a stream ID that
10379 /// exceeds `STREAM_ID_MAX` for either side, no matter where the
10380 /// watermark sits. This walks every value in a neighbourhood of the
10381 /// boundary to rule out off-by-one errors.
10382 #[test]
10383 fn test_next_stream_id_never_exceeds_stream_id_max() {
10384 for last in (STREAM_ID_MAX - 4)..=(STREAM_ID_MAX + 4) {
10385 for is_client in [true, false] {
10386 if let Some((issued, next)) = next_stream_id(last, is_client) {
10387 assert!(
10388 issued <= STREAM_ID_MAX,
10389 "issued id {issued} exceeds STREAM_ID_MAX (last={last}, is_client={is_client})"
10390 );
10391 // `next` is the post-allocation watermark and may sit at
10392 // STREAM_ID_MAX + 1 — the very next call must then return None.
10393 if next > STREAM_ID_MAX {
10394 assert!(
10395 next_stream_id(next, is_client).is_none(),
10396 "second call after final slot must report exhaustion"
10397 );
10398 }
10399 }
10400 }
10401 }
10402 }
10403
10404 /// The helper's `is_client` flag must cleanly split the ID space so that
10405 /// a client and a server peered on the same connection cannot collide.
10406 /// Given the same `last_stream_id`, the two parities must differ by 1.
10407 #[test]
10408 fn test_next_stream_id_client_server_parities_disjoint() {
10409 for last in [0u32, 2, 4, 10, 100, 1_000_000, STREAM_ID_MAX - 3] {
10410 let (client_id, _) = next_stream_id(last, true).unwrap();
10411 let (server_id, _) = next_stream_id(last, false).unwrap();
10412 assert_eq!(client_id & 1, 1);
10413 assert_eq!(server_id & 1, 0);
10414 assert_eq!(client_id.abs_diff(server_id), 1);
10415 }
10416 }
10417
10418 // ── LIFECYCLE §9 invariant 16: any_stream_id_matches ─────────────────
10419 //
10420 // Covers the iteration dispatch used by `any_stream_has_pending_back`.
10421 // Testing the probe directly against a synthetic closure keeps the
10422 // tests independent of the full `Stream` fixture (which requires a
10423 // `Pool` and a fully-built `HttpContext`).
10424
10425 #[test]
10426 fn test_any_stream_id_matches_empty_map_is_false() {
10427 let streams: BTreeMap<StreamId, GlobalStreamId> = BTreeMap::new();
10428 assert!(!any_stream_id_matches(&streams, |_| true));
10429 }
10430
10431 #[test]
10432 fn test_any_stream_id_matches_all_probe_false_is_false() {
10433 let mut streams: BTreeMap<StreamId, GlobalStreamId> = BTreeMap::new();
10434 streams.insert(1, 0);
10435 streams.insert(3, 1);
10436 streams.insert(5, 2);
10437 assert!(!any_stream_id_matches(&streams, |_| false));
10438 }
10439
10440 #[test]
10441 fn test_any_stream_id_matches_any_probe_true_is_true() {
10442 let mut streams: BTreeMap<StreamId, GlobalStreamId> = BTreeMap::new();
10443 streams.insert(1, 0);
10444 streams.insert(3, 1);
10445 streams.insert(5, 2);
10446 // Probe is true only for GlobalStreamId == 1 (i.e. StreamId 3).
10447 assert!(any_stream_id_matches(&streams, |gid| gid == 1));
10448 }
10449
10450 #[test]
10451 fn test_any_stream_id_matches_single_entry() {
10452 let mut streams: BTreeMap<StreamId, GlobalStreamId> = BTreeMap::new();
10453 streams.insert(42, 7);
10454 assert!(any_stream_id_matches(&streams, |gid| gid == 7));
10455 assert!(!any_stream_id_matches(&streams, |gid| gid == 8));
10456 }
10457
10458 #[test]
10459 fn test_any_stream_id_matches_short_circuits() {
10460 let mut streams: BTreeMap<StreamId, GlobalStreamId> = BTreeMap::new();
10461 streams.insert(1, 0);
10462 streams.insert(3, 1);
10463 streams.insert(5, 2);
10464 streams.insert(7, 3);
10465 let mut calls = 0usize;
10466 let result = any_stream_id_matches(&streams, |_| {
10467 calls += 1;
10468 true
10469 });
10470 assert!(result);
10471 // `Iterator::any` short-circuits on the first `true` — so the probe
10472 // must fire at most once in this construction.
10473 assert_eq!(calls, 1);
10474 }
10475
10476 // ── cumulative-stall budget decision (fc_stall_budget_decision) ──
10477
10478 #[test]
10479 fn test_fc_stall_budget_open_window_always_clears() {
10480 // A genuinely open send window is a real un-stall, regardless of prior
10481 // accumulated progress or this pass's drain.
10482 assert_eq!(
10483 fc_stall_budget_decision(false, 0, None),
10484 FcStallAction::Clear
10485 );
10486 assert_eq!(
10487 fc_stall_budget_decision(false, 1, Some(5)),
10488 FcStallAction::Clear
10489 );
10490 assert_eq!(
10491 fc_stall_budget_decision(false, i32::MAX, Some(FC_STALL_CLEAR_FLOOR)),
10492 FcStallAction::Clear
10493 );
10494 }
10495
10496 #[test]
10497 fn test_fc_stall_budget_blocked_arms_and_accumulates() {
10498 // First blocked pass arms with this pass's drain.
10499 assert_eq!(
10500 fc_stall_budget_decision(true, 1, None),
10501 FcStallAction::Arm { progress: 1 }
10502 );
10503 // A blocked pass with no drain keeps the accumulator unchanged, so the
10504 // deadline keeps aging (a window-0 stall makes consumed == 0).
10505 assert_eq!(
10506 fc_stall_budget_decision(true, 0, Some(42)),
10507 FcStallAction::Arm { progress: 42 }
10508 );
10509 // Negative `consumed` is clamped to 0 (defensive; converter.window only
10510 // shrinks, so consumed is >= 0 in practice).
10511 assert_eq!(
10512 fc_stall_budget_decision(true, -10, Some(7)),
10513 FcStallAction::Arm { progress: 7 }
10514 );
10515 }
10516
10517 #[test]
10518 fn test_fc_stall_budget_floor_clears() {
10519 // Reaching the floor in a single pass (a full DATA frame of real
10520 // delivery) clears the deadline.
10521 assert_eq!(
10522 fc_stall_budget_decision(true, FC_STALL_CLEAR_FLOOR as i32, None),
10523 FcStallAction::Clear
10524 );
10525 // Exactly one byte below the floor still arms.
10526 assert_eq!(
10527 fc_stall_budget_decision(true, (FC_STALL_CLEAR_FLOOR - 1) as i32, None),
10528 FcStallAction::Arm {
10529 progress: FC_STALL_CLEAR_FLOOR - 1
10530 }
10531 );
10532 // Prior progress plus this pass crossing the floor clears.
10533 assert_eq!(
10534 fc_stall_budget_decision(true, 1, Some(FC_STALL_CLEAR_FLOOR - 1)),
10535 FcStallAction::Clear
10536 );
10537 }
10538
10539 #[test]
10540 fn test_fc_stall_budget_wu_drip_ages_until_floor() {
10541 // The WINDOW_UPDATE(+1) closure: a 1-byte-per-pass drip must keep the
10542 // deadline armed (aging) for the whole run up to the floor and only
10543 // clear on the pass that reaches it — so a drip granting < floor bytes
10544 // per idle period is reaped, never kept alive. This is the unit-level
10545 // proof that the budget closes the WINDOW_UPDATE-drip vector.
10546 let mut progress: Option<usize> = None;
10547 for pass in 1..FC_STALL_CLEAR_FLOOR {
10548 match fc_stall_budget_decision(true, 1, progress) {
10549 FcStallAction::Arm { progress: p } => {
10550 assert_eq!(p, pass, "drip accumulator off at pass {pass}");
10551 progress = Some(p);
10552 }
10553 FcStallAction::Clear => panic!("drip cleared the deadline early at pass {pass}"),
10554 }
10555 }
10556 // The pass that reaches the floor finally clears.
10557 assert_eq!(
10558 fc_stall_budget_decision(true, 1, progress),
10559 FcStallAction::Clear
10560 );
10561 }
10562
10563 // ── flow-control-stall reaper union (h2_stream_table::collect_timed_out) ──
10564 //
10565 // The two-guard union (bidirectional-silence + flow-control-stall),
10566 // its dedup, its rst_sent/untracked filtering, and its determinism
10567 // (ascending stream_id order — the regression test for this
10568 // extraction's fix) are now unit-tested directly against
10569 // `H2StreamTable::collect_timed_out` in `h2_stream_table.rs`'s own test
10570 // module, without needing a full `ConnectionH2` fixture.
10571
10572 // ── LIFECYCLE §9 invariant 16: any_stream_has_pending_back ───────────
10573
10574 /// Build a minimal `Stream` for invariant-16 probing. Uses the pool
10575 /// plumbing so `back.blocks` / `back.out` exist; every other field is
10576 /// default-valued because the predicate only reads the back buffer.
10577 fn make_stream_for_invariant_16(pool: &Rc<RefCell<Pool>>, session_ulid: Ulid) -> Stream {
10578 let http_ctx = HttpContext {
10579 keep_alive_backend: true,
10580 keep_alive_frontend: true,
10581 sticky_session_found: None,
10582 method: None,
10583 authority: None,
10584 path: None,
10585 status: None,
10586 reason: None,
10587 user_agent: None,
10588 x_request_id: None,
10589 xff_chain: None,
10590 #[cfg(feature = "opentelemetry")]
10591 otel: None,
10592 closing: false,
10593 session_id: session_ulid,
10594 id: Ulid::generate(),
10595 backend_id: None,
10596 cluster_id: None,
10597 cluster_metrics_incarnation: Default::default(),
10598 affinity_key: None,
10599 protocol: Protocol::HTTPS,
10600 public_address: "127.0.0.1:0".parse().unwrap(),
10601 session_address: None,
10602 sticky_name: String::new(),
10603 sticky_session: None,
10604 backend_address: None,
10605 tls_server_name: None,
10606 tls_cert_names: None,
10607 strict_sni_binding: false,
10608 elide_x_real_ip: false,
10609 send_x_real_ip: false,
10610 forwarded_headers: sozu_command_lib::proto::command::ForwardedHeaders::Both,
10611 max_trailer_fields: 128,
10612 trailer_fields: 0,
10613 tls_version: None,
10614 tls_cipher: None,
10615 tls_alpn: None,
10616 sozu_id_header: String::from("Sozu-Id"),
10617 redirect_location: None,
10618 www_authenticate: None,
10619 original_authority: None,
10620 headers_response: Vec::new(),
10621 retry_after_seconds: None,
10622 frontend_redirect_template: None,
10623 redirect_status: None,
10624 tags: None,
10625 forwarding_hop: None,
10626 access_log_message: None,
10627 backends_unavailable: false,
10628 };
10629 Stream::new(
10630 &mut PoolBufferSource::new(Rc::downgrade(pool)),
10631 http_ctx,
10632 crate::protocol::mux::test_support::test_answers(),
10633 65_535,
10634 )
10635 .expect("pool should have capacity for two buffers")
10636 }
10637
10638 fn make_pool_for_invariant_16() -> Rc<RefCell<Pool>> {
10639 // Two buffer slots per stream (front + back), ten stream slots is
10640 // plenty for the tests below.
10641 Rc::new(RefCell::new(Pool::with_capacity(4, 20, 16_384)))
10642 }
10643
10644 /// What `handle_data_frame` does with the DATA payload of a backend
10645 /// response: a 204 or a 304 cannot carry content and is reset, even to
10646 /// HEAD; a response to HEAD only SHOULD NOT and its payload is
10647 /// discarded (RFC 9110 §6.4.1, §9.3.2, §15.3.5, §15.4.5).
10648 #[test]
10649 fn a_backend_response_content_is_forbidden_for_204_and_304_and_discarded_for_head() {
10650 use crate::protocol::kawa_h1::parser::Method;
10651 let pool = make_pool_for_invariant_16();
10652 let mut stream = make_stream_for_invariant_16(&pool, Ulid::generate());
10653 let cases = [
10654 (
10655 Some(Method::Head),
10656 Some(200),
10657 BackendResponseContent::Discarded,
10658 ),
10659 (
10660 Some(Method::Head),
10661 Some(204),
10662 BackendResponseContent::Forbidden,
10663 ),
10664 (
10665 Some(Method::Get),
10666 Some(204),
10667 BackendResponseContent::Forbidden,
10668 ),
10669 (
10670 Some(Method::Get),
10671 Some(304),
10672 BackendResponseContent::Forbidden,
10673 ),
10674 (
10675 Some(Method::Get),
10676 Some(200),
10677 BackendResponseContent::Forwarded,
10678 ),
10679 (
10680 Some(Method::Post),
10681 Some(205),
10682 BackendResponseContent::Forwarded,
10683 ),
10684 (None, None, BackendResponseContent::Forwarded),
10685 ];
10686 for (method, status, expected) in cases {
10687 stream.context.method = method.clone();
10688 stream.context.status = status;
10689 assert_eq!(
10690 ConnectionH2::backend_response_content(&stream.context),
10691 expected,
10692 "{method:?} {status:?}"
10693 );
10694 }
10695 }
10696
10697 #[test]
10698 fn test_any_stream_has_pending_back_empty_map_is_false() {
10699 let pool = make_pool_for_invariant_16();
10700 let ulid = Ulid::generate();
10701 let streams_map: BTreeMap<StreamId, GlobalStreamId> = BTreeMap::new();
10702 let context_streams = vec![make_stream_for_invariant_16(&pool, ulid)];
10703 assert!(!any_stream_has_pending_back(&streams_map, &context_streams));
10704 }
10705
10706 #[test]
10707 fn test_any_stream_has_pending_back_all_drained_is_false() {
10708 let pool = make_pool_for_invariant_16();
10709 let ulid = Ulid::generate();
10710 let context_streams = vec![
10711 make_stream_for_invariant_16(&pool, ulid),
10712 make_stream_for_invariant_16(&pool, ulid),
10713 ];
10714 let mut streams_map: BTreeMap<StreamId, GlobalStreamId> = BTreeMap::new();
10715 streams_map.insert(1, 0);
10716 streams_map.insert(3, 1);
10717 // Both freshly-built streams have empty back.out and back.blocks
10718 // (Kawa::new starts with empty deques).
10719 assert!(!any_stream_has_pending_back(&streams_map, &context_streams));
10720 }
10721
10722 #[test]
10723 fn test_any_stream_has_pending_back_unknown_gid_is_false() {
10724 // LIFECYCLE invariant 16 defence-in-depth: an unknown
10725 // `GlobalStreamId` during a stream-removal race must not panic;
10726 // `.get()` must short-circuit to `false`.
10727 let pool = make_pool_for_invariant_16();
10728 let ulid = Ulid::generate();
10729 let context_streams = vec![make_stream_for_invariant_16(&pool, ulid)];
10730 let mut streams_map: BTreeMap<StreamId, GlobalStreamId> = BTreeMap::new();
10731 // GlobalStreamId 42 is out of range for the 1-element slice above.
10732 streams_map.insert(7, 42);
10733 assert!(!any_stream_has_pending_back(&streams_map, &context_streams));
10734 }
10735
10736 #[test]
10737 fn test_any_stream_has_pending_back_with_pending_blocks_is_true() {
10738 let pool = make_pool_for_invariant_16();
10739 let ulid = Ulid::generate();
10740 let mut stream = make_stream_for_invariant_16(&pool, ulid);
10741 // Push one dummy block — any Block variant is fine; the predicate
10742 // only checks `blocks.is_empty()`.
10743 stream.back.blocks.push_back(kawa::Block::StatusLine);
10744 let mut streams_map: BTreeMap<StreamId, GlobalStreamId> = BTreeMap::new();
10745 streams_map.insert(1, 0);
10746 assert!(any_stream_has_pending_back(&streams_map, &[stream]));
10747 }
10748
10749 #[test]
10750 fn test_any_stream_has_pending_back_with_pending_out_is_true() {
10751 let pool = make_pool_for_invariant_16();
10752 let ulid = Ulid::generate();
10753 let mut stream = make_stream_for_invariant_16(&pool, ulid);
10754 // Non-empty out buffer with no blocks.
10755 stream
10756 .back
10757 .out
10758 .push_back(kawa::OutBlock::Store(kawa::Store::Static(b"partial frame")));
10759 let mut streams_map: BTreeMap<StreamId, GlobalStreamId> = BTreeMap::new();
10760 streams_map.insert(1, 0);
10761 assert!(any_stream_has_pending_back(&streams_map, &[stream]));
10762 }
10763
10764 // ── Mid-pass ready-incremental census consistency ───────────────────
10765 //
10766 // The three scalar bucket-decrement tests moved to `h2_scheduler.rs`.
10767 // They were named after `ready_incremental_by_urgency`, the local
10768 // `HashMap` this step replaced with `ReadyIncrementalCensus`; no symbol
10769 // of that name exists anywhere in the tree any more.
10770 // They used to re-implement the `saturating_sub` + bucket-scoped
10771 // decrement inline in the test body — a copy of the scheduler's
10772 // arithmetic, which could not catch the scheduler getting it wrong.
10773 // They now call `ReadyIncrementalCensus::note_ineligible` and
10774 // `::incremental_peer_count`, which is the code `write_streams` runs.
10775
10776 // ── h2.streams.ready_incremental.by_urgency aggregate ────────────────
10777
10778 /// Read the process-local `h2.streams.ready_incremental.by_urgency` gauge,
10779 /// treating an absent key as 0. `dump_local_proxy_metrics` is a
10780 /// non-draining filter over the proxy `MetricsMap`, so repeated reads are
10781 /// side-effect free and the key is the raw metric name.
10782 fn ready_incremental_gauge() -> i64 {
10783 use sozu_command::proto::command::filtered_metrics::Inner;
10784 crate::metrics::METRICS.with(|metrics| {
10785 metrics
10786 .borrow_mut()
10787 .dump_local_proxy_metrics()
10788 .get(names::h2::STREAMS_READY_INCREMENTAL_BY_URGENCY)
10789 .and_then(|fm| fm.inner.as_ref())
10790 .and_then(|inner| match inner {
10791 Inner::Gauge(v) => Some(*v as i64),
10792 _ => None,
10793 })
10794 .unwrap_or(0)
10795 })
10796 }
10797
10798 /// A [`BufferSource`] that hands over pooled wire buffers exactly as the
10799 /// real one does but refuses every scratch request.
10800 ///
10801 /// It exists to separate the two halves of the contract. Refusing
10802 /// `checkout` is already reachable by sizing a pool, and the H2
10803 /// simulator's exhaustion scenario does exactly that. Refusing `scratch`
10804 /// is reachable ONLY through a caller-supplied source: the proxy's own
10805 /// [`PoolBufferSource`] hands out a fresh `Vec` and never says no. That
10806 /// asymmetry is the point of the trait being caller-implemented rather
10807 /// than a method on `Pool`, so it needs a test that could not be written
10808 /// without it.
10809 struct ScratchStarvedSource(PoolBufferSource);
10810
10811 impl super::super::buffer_source::BufferSource for ScratchStarvedSource {
10812 fn checkout(&mut self) -> Option<crate::pool::Checkout> {
10813 self.0.checkout()
10814 }
10815
10816 fn scratch(&mut self) -> Option<Vec<u8>> {
10817 None
10818 }
10819 }
10820
10821 fn loopback_socket_handle() -> mio::net::TcpStream {
10822 mio::net::TcpStream::connect(
10823 format!("127.0.0.1:{}", crate::testing::provide_port())
10824 .parse()
10825 .expect("loopback address must parse"),
10826 )
10827 .expect("mio connect must return a socket handle")
10828 }
10829
10830 fn connection_from_source(
10831 buffers: &mut dyn super::super::buffer_source::BufferSource,
10832 ) -> Option<H2Shell<mio::net::TcpStream>> {
10833 H2Shell::new(
10834 Ulid::generate(),
10835 loopback_socket_handle(),
10836 Position::Server,
10837 buffers,
10838 H2FloodConfig::default(),
10839 H2ConnectionConfig::default(),
10840 Duration::from_secs(30),
10841 None,
10842 Duration::from_secs(30),
10843 Some((H2StreamId::Zero, CLIENT_PREFACE_SIZE)),
10844 Ready::READABLE | Ready::HUP | Ready::ERROR,
10845 )
10846 }
10847
10848 /// The HPACK half of the ownership contract is real, not decorative: the
10849 /// codec pair and its scratch buffers are acquired from the connection's
10850 /// buffer source, and a source that refuses one refuses the connection.
10851 ///
10852 /// Both arms run against the SAME pool, with enough buffers for the
10853 /// connection's stream-0 checkout. So the only difference between the two
10854 /// outcomes is the source's answer to `scratch`, which is what stops this
10855 /// from passing for the unrelated reason that the pool ran dry.
10856 #[test]
10857 fn a_source_that_refuses_scratch_refuses_the_connection() {
10858 let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
10859
10860 assert!(
10861 connection_from_source(&mut PoolBufferSource::new(Rc::downgrade(&pool))).is_some(),
10862 "the pool has free buffers, so a source that grants scratch must build a connection",
10863 );
10864 assert!(
10865 connection_from_source(&mut ScratchStarvedSource(PoolBufferSource::new(
10866 Rc::downgrade(&pool)
10867 )))
10868 .is_none(),
10869 "a source that refuses the HPACK scratch must refuse the connection: the codec pair \
10870 is under the same ownership contract as the wire buffers, not allocated behind it",
10871 );
10872 }
10873
10874 /// `h2.streams.ready_incremental.by_urgency` is an aggregate across every
10875 /// live H2 connection, so a connection must hand back exactly what it
10876 /// contributed, on every close path. `H2Shell`'s [`Drop`] is the single
10877 /// decrement site; this pins the round trip.
10878 ///
10879 /// Before this was folded into `last_gauge_snapshot`, the metric was an
10880 /// absolute `gauge!` set from inside `write_streams`: connection B
10881 /// overwrote connection A's value, nothing was ever released, and the
10882 /// dashboard read "whatever the last writer wrote" instead of a sum.
10883 ///
10884 /// `METRICS` is a thread-local, so this asserts the DELTA around one
10885 /// connection's lifetime — robust to any starting value.
10886 ///
10887 /// To SEE THIS RED: delete `self.core.release_connection_gauges();`
10888 /// from `impl Drop for H2Shell`, leaving the
10889 /// `self.record_pending_metrics();` below it. The live-delta assertion
10890 /// still passes — the shell still records what the core queued — and the
10891 /// post-drop one fails with `left: 3, right: 0`: the connection's
10892 /// contribution outliving the connection, which is the
10893 /// aggregate drifting upward by 3 for the rest of the worker's life.
10894 #[test]
10895 fn dropping_a_connection_releases_its_ready_incremental_contribution() {
10896 let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
10897
10898 // A non-blocking connect to a (likely unused) loopback port returns a
10899 // real socket handle immediately, regardless of whether the connection
10900 // completes; nothing below reads or writes it.
10901 let socket = mio::net::TcpStream::connect(
10902 format!("127.0.0.1:{}", crate::testing::provide_port())
10903 .parse()
10904 .expect("loopback address must parse"),
10905 )
10906 .expect("mio connect must return a socket handle");
10907
10908 let before = ready_incremental_gauge();
10909
10910 let mut connection = H2Shell::new(
10911 Ulid::generate(),
10912 socket,
10913 Position::Server,
10914 &mut PoolBufferSource::new(Rc::downgrade(&pool)),
10915 H2FloodConfig::default(),
10916 H2ConnectionConfig::default(),
10917 Duration::from_secs(30),
10918 None,
10919 Duration::from_secs(30),
10920 Some((H2StreamId::Zero, CLIENT_PREFACE_SIZE)),
10921 Ready::READABLE | Ready::HUP | Ready::ERROR,
10922 )
10923 .expect("a pool with free buffers must yield an H2 connection");
10924
10925 // What the tail of `write_streams` writes once the pass's bucket map
10926 // is final, published by the `gauge_connection_state` call that
10927 // follows it — then handed to the aggregator by the shell, which is
10928 // what `H2Shell::settled` does at the end of every real pass.
10929 connection.core.ready_incremental_streams = 3;
10930 connection.core.gauge_connection_state();
10931 connection.record_pending_metrics();
10932
10933 assert_eq!(
10934 ready_incremental_gauge() - before,
10935 3,
10936 "gauge_connection_state must add this connection's ready-incremental \
10937 count to the aggregate as a signed delta"
10938 );
10939
10940 drop(connection);
10941
10942 assert_eq!(
10943 ready_incremental_gauge(),
10944 before,
10945 "Drop must subtract exactly the contribution the connection made, \
10946 returning the aggregate to its prior value"
10947 );
10948 }
10949
10950 /// The core computes the gauge arithmetic and stops there: writing the
10951 /// worker-local `METRICS` aggregator is the shell's step, through
10952 /// [`H2Shell::record_pending_metrics`].
10953 ///
10954 /// This is the property that makes the H2 core drivable by a deterministic
10955 /// simulator, which is the whole point of returning the metric rather than
10956 /// emitting it: a driver with no aggregator sees the same
10957 /// [`MetricEvent`]s that production folds into `METRICS`, and can fold
10958 /// them into a shadow model instead. `protocol/udp/`'s core already works
10959 /// this way and `sim/tests/udp_simulation.rs` already reads it that way.
10960 ///
10961 /// The sibling test above pins the arithmetic; this one pins *who writes*,
10962 /// which no assertion on the aggregate's value can distinguish — a core
10963 /// that emitted directly and a shell that records what the core queued
10964 /// leave the aggregate identical.
10965 ///
10966 /// To SEE THIS RED: in [`ConnectionH2::gauge_connection_state`], put
10967 /// `gauge_add!(names::h2::STREAMS_READY_INCREMENTAL_BY_URGENCY, dr);`
10968 /// back in place of the
10969 /// `self.metric_events.push(MetricEvent::StreamsReadyIncrementalByUrgency(dr));`
10970 /// push. The first assertion then fails with `left: <before + 5>, right:
10971 /// <before>` — the core reaching the aggregator behind the shell's back.
10972 #[test]
10973 fn the_core_queues_gauge_deltas_instead_of_writing_the_metrics_registry() {
10974 let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
10975 let before = ready_incremental_gauge();
10976
10977 let mut connection =
10978 connection_from_source(&mut PoolBufferSource::new(Rc::downgrade(&pool)))
10979 .expect("a pool with free buffers must yield an H2 connection");
10980
10981 connection.core.ready_incremental_streams = 5;
10982 connection.core.gauge_connection_state();
10983
10984 assert_eq!(
10985 ready_incremental_gauge(),
10986 before,
10987 "the core must not write METRICS: `gauge_connection_state` queues \
10988 a MetricEvent and the shell decides when it is recorded"
10989 );
10990
10991 let queued: Vec<MetricEvent> = connection.core.drain_metric_events().collect();
10992 assert!(
10993 queued.contains(&MetricEvent::StreamsReadyIncrementalByUrgency(5)),
10994 "the queued events must carry the signed delta the core computed, \
10995 got {queued:?}"
10996 );
10997 assert_eq!(
10998 ready_incremental_gauge(),
10999 before,
11000 "draining is not recording: taking the events out of the core must \
11001 not move the aggregate either"
11002 );
11003
11004 for event in queued {
11005 record_metric(event);
11006 }
11007
11008 assert_eq!(
11009 ready_incremental_gauge() - before,
11010 5,
11011 "recording the drained events must produce exactly the aggregate \
11012 the old in-core `gauge_add!` produced"
11013 );
11014
11015 drop(connection);
11016
11017 assert_eq!(
11018 ready_incremental_gauge(),
11019 before,
11020 "the released snapshot must travel the same queue and rebalance to \
11021 the prior value"
11022 );
11023 }
11024
11025 /// Read a process-local counter by its raw metric key, treating an absent
11026 /// key as 0. Counter sibling of [`ready_incremental_gauge`], and
11027 /// `dump_local_proxy_metrics` is non-draining here too, so repeated reads
11028 /// are side-effect free.
11029 fn proxy_counter(key: &str) -> i64 {
11030 use sozu_command::proto::command::filtered_metrics::Inner;
11031 crate::metrics::METRICS.with(|metrics| {
11032 metrics
11033 .borrow_mut()
11034 .dump_local_proxy_metrics()
11035 .get(key)
11036 .and_then(|fm| fm.inner.as_ref())
11037 .and_then(|inner| match inner {
11038 Inner::Count(v) => Some(*v),
11039 _ => None,
11040 })
11041 .unwrap_or(0)
11042 })
11043 }
11044
11045 /// A counter the core queues is recorded when the entry point that
11046 /// produced it RETURNS — not whenever the connection is eventually
11047 /// dropped.
11048 ///
11049 /// This is the property the gauges did not need. A gauge delta recorded
11050 /// late still sums to the same value, so slice one could be correct with a
11051 /// drain only in `settled` and `Drop`. A counter recorded late is a
11052 /// counter attributed to the wrong minute: `LocalDrain` is cumulative and
11053 /// scraped periodically, so deferring one to connection teardown moves it
11054 /// past however many scrapes the connection outlives. `Drop` remains the
11055 /// backstop that stops it being *lost*; this pins that the backstop is not
11056 /// where it normally happens.
11057 ///
11058 /// `graceful_goaway` is the cheapest real producer to drive: one public
11059 /// call on a fresh connection reaches `ConnectionH2::send_initial_goaway`,
11060 /// which queues both `MetricEvent::GoAwaySent` and
11061 /// `MetricEvent::GoAwayFrameSent`.
11062 ///
11063 /// To SEE THIS RED: delete `self.record_pending_metrics();` from
11064 /// `H2Shell::settled`. Both assertions before `drop` fail with
11065 /// `left: 0, right: 1` — the counters sitting in the core's queue,
11066 /// arriving only when the connection is torn down.
11067 #[test]
11068 fn a_queued_counter_is_recorded_when_its_entry_point_returns() {
11069 let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
11070 let sent_before = proxy_counter("h2.goaway.sent.no_error");
11071 let frames_before = proxy_counter(names::h2::FRAMES_TX_GOAWAY);
11072
11073 let mut connection =
11074 connection_from_source(&mut PoolBufferSource::new(Rc::downgrade(&pool)))
11075 .expect("a pool with free buffers must yield an H2 connection");
11076
11077 connection.graceful_goaway(Instant::now());
11078
11079 assert_eq!(
11080 proxy_counter("h2.goaway.sent.no_error") - sent_before,
11081 1,
11082 "the GOAWAY decision must reach METRICS when `graceful_goaway` \
11083 returns, not at teardown"
11084 );
11085 assert_eq!(
11086 proxy_counter(names::h2::FRAMES_TX_GOAWAY) - frames_before,
11087 1,
11088 "so must the frame the serializer actually wrote"
11089 );
11090 }
11091
11092 /// One `MetricEvent::RstStreamSent` records BOTH counters its two macro
11093 /// lines used to record: the frame-type total and the per-error-code
11094 /// breakdown.
11095 ///
11096 /// Folding two macro lines into one event is only safe where both were
11097 /// unconditional and adjacent, which is why the GOAWAY pair was NOT folded
11098 /// — `h2.frames.tx.goaway` sits inside `serializer::gen_goaway`'s `Ok`
11099 /// arm while `h2.goaway.sent.*` fires before it, so one event would record
11100 /// a frame on the wire that a serialisation failure never wrote. The RST
11101 /// pair has no such gap, and this pins that folding it kept both counters.
11102 ///
11103 /// To SEE THIS RED: delete either line from the
11104 /// `MetricEvent::RstStreamSent` arm of `record_metric`. The surviving
11105 /// assertion passes and the other fails with `left: 0, right: 1` — half a
11106 /// merge, which is exactly what a merged pair can silently become.
11107 #[test]
11108 fn one_rst_stream_sent_event_records_both_of_its_counters() {
11109 let frames_before = proxy_counter(names::h2::FRAMES_TX_RST_STREAM);
11110 let by_code_before = proxy_counter(metric_for_rst_stream_sent(H2Error::Cancel));
11111
11112 record_metric(MetricEvent::RstStreamSent(H2Error::Cancel));
11113
11114 assert_eq!(
11115 proxy_counter(names::h2::FRAMES_TX_RST_STREAM) - frames_before,
11116 1,
11117 "the frame-type total must still tick"
11118 );
11119 assert_eq!(
11120 proxy_counter(metric_for_rst_stream_sent(H2Error::Cancel)) - by_code_before,
11121 1,
11122 "and so must the per-error-code breakdown"
11123 );
11124 }
11125
11126 // ── Clock hygiene: every deadline reads `ConnectionH2::now` ──────────
11127 //
11128 // `Mux` is the only clock sampler; the H2 core reads the snapshot it
11129 // mirrors into `ConnectionH2::now`. A reintroduced `Instant::now()` in
11130 // the core would still "work" in production — it would just be reading a
11131 // second clock — so these two tests advance ONLY the connection's
11132 // snapshot and prove the decision moves with it. Neither sleeps.
11133
11134 // ── TLS backpressure: the GoAway truncation vector (#1454) ──────────
11135 //
11136 // Until this harness existed, NO test anywhere instantiated a
11137 // `ConnectionH2` over a handler whose `socket_wants_write()` could return
11138 // `true`. `mio::net::TcpStream` and `SessionTcpStream` both take the
11139 // trait's `false` default and only `FrontRustls` overrides it, so every
11140 // branch that asks "does rustls still hold records?" was statically dead
11141 // in the test suite — including the one whose own comment calls it the
11142 // primary truncation vector. These are the first tests that branch class
11143 // has ever had against a handler that answers `true`.
11144
11145 /// A `SocketHandler` that models rustls-over-a-blocked-kernel: it holds
11146 /// `pending` records, and each empty-buffer flush drains `drain_per_flush`
11147 /// of them. `drain_per_flush = 0` is a kernel that accepts nothing, which
11148 /// is the backpressure case; a positive value is a kernel that takes them.
11149 ///
11150 /// Modelling the state rather than scripting an answer sequence is
11151 /// deliberate: the close path queries `socket_wants_write()` a number of
11152 /// times that depends on which branches it takes, so a positional script
11153 /// would pin the query COUNT instead of the behaviour and would have to be
11154 /// rewritten by anyone who added a query.
11155 struct BackpressuredTlsSocket {
11156 stream: mio::net::TcpStream,
11157 pending: std::cell::Cell<usize>,
11158 drain_per_flush: usize,
11159 flushes: std::cell::Cell<usize>,
11160 /// An empty-buffer flush that leaves records behind answers
11161 /// `WouldBlock`, as `FrontRustls` does when the kernel refused them,
11162 /// instead of `Continue`. Off by default, so every test written
11163 /// before this field is byte-for-byte unaffected.
11164 refuse_flush: bool,
11165 /// Scripted `(size, status)` answers for `socket_write_vectored`,
11166 /// consumed front to back. An EMPTY script — the default, and what
11167 /// every test written before this field had — delegates to the real
11168 /// loopback socket, so those tests are byte-for-byte unaffected.
11169 ///
11170 /// The record model above cannot express the pair this scripts.
11171 /// `pending`/`drain_per_flush` answer "does rustls still hold
11172 /// records?"; they say nothing about what a *vectored* write
11173 /// reported, because that method delegated to the kernel and the
11174 /// kernel only ever answers `(0, WouldBlock)` when it blocks.
11175 /// `FrontRustls` does not: `socket_write_vectored` accumulates
11176 /// `buffered_size` from `session.writer().write(..)` — plaintext
11177 /// rustls took off the caller's hands — and separately sets
11178 /// `can_write = false` when `write_tls` hits `WouldBlock` against the
11179 /// kernel (`socket.rs`). The two are independent, so
11180 /// `(size > 0, WouldBlock)` is a shape ONLY the TLS handler returns.
11181 /// That is why the script lives on this fixture rather than on a
11182 /// second one: it is the same handler answering the same question the
11183 /// record model already answers, one layer down.
11184 ///
11185 /// Each `size` is a CAP, clamped to what the gather actually offered:
11186 /// a scripted count above the offer would trip the write shell's own
11187 /// `debug_assert!(size <= offered)`, and reddening through a
11188 /// production assertion is the tree noticing rather than the test.
11189 vectored_script: std::collections::VecDeque<(usize, SocketResult)>,
11190 /// Scripted `(size, status)` answers for a NON-EMPTY `socket_write`,
11191 /// consumed front to back exactly like `vectored_script`. An empty
11192 /// script — the default — delegates to the real loopback socket, so
11193 /// every test written before this field is byte-for-byte unaffected.
11194 ///
11195 /// The record model cannot express this either. `pending` /
11196 /// `drain_per_flush` answer "does rustls still hold records?", which
11197 /// is the EMPTY-buffer flush question; this one is "did the kernel
11198 /// take the bytes `flush_output_to_socket` handed it?". A loopback
11199 /// socket with default buffers always takes 13 bytes of
11200 /// WINDOW_UPDATE, so the stalled branch of the control-frame drains
11201 /// is unreachable without scripting the answer.
11202 ///
11203 /// Each `size` is a CAP, clamped to what the caller actually offered:
11204 /// `flush_output_to_socket` feeds the answer straight to
11205 /// `H2Output::consume`, which asserts it never passes the queue.
11206 write_script: std::collections::VecDeque<(usize, SocketResult)>,
11207 /// How many times `socket_write_vectored` was called, whether the
11208 /// answer came from the script or from the delegated loopback socket.
11209 ///
11210 /// Counting BOTH is what makes this a falsifiable witness. Counting
11211 /// only the scripted answers bounds the total by the script's length,
11212 /// so an assertion that the loop did NOT go round again could only
11213 /// ever deviate downwards and could never fail — and the extra
11214 /// delegated rounds are exactly what such an assertion has to be able
11215 /// to see.
11216 vectored_calls: usize,
11217 /// How many NON-EMPTY `socket_write` calls reached this handler. On
11218 /// `FrontRustls` each one ends in its own `write_tls`, so each one is
11219 /// one `writev(2)` on the wire; an empty-buffer flush is counted by
11220 /// `flushes` instead.
11221 writes: usize,
11222 }
11223
11224 impl BackpressuredTlsSocket {
11225 fn new(stream: mio::net::TcpStream, pending: usize, drain_per_flush: usize) -> Self {
11226 Self {
11227 stream,
11228 pending: std::cell::Cell::new(pending),
11229 drain_per_flush,
11230 flushes: std::cell::Cell::new(0),
11231 refuse_flush: false,
11232 vectored_script: std::collections::VecDeque::new(),
11233 write_script: std::collections::VecDeque::new(),
11234 vectored_calls: 0,
11235 writes: 0,
11236 }
11237 }
11238 }
11239
11240 impl SocketHandler for BackpressuredTlsSocket {
11241 fn socket_read(&mut self, buf: &mut [u8]) -> (usize, SocketResult) {
11242 self.stream.socket_read(buf)
11243 }
11244
11245 fn socket_write(&mut self, buf: &[u8]) -> (usize, SocketResult) {
11246 if buf.is_empty() {
11247 self.flushes.set(self.flushes.get() + 1);
11248 let drained = self.drain_per_flush.min(self.pending.get());
11249 self.pending.set(self.pending.get() - drained);
11250 if self.refuse_flush && self.pending.get() > 0 {
11251 return (0, SocketResult::WouldBlock);
11252 }
11253 return (0, SocketResult::Continue);
11254 }
11255 self.writes += 1;
11256 match self.write_script.pop_front() {
11257 Some((cap, status)) => (cap.min(buf.len()), status),
11258 None => self.stream.socket_write(buf),
11259 }
11260 }
11261
11262 fn socket_write_vectored(&mut self, bufs: &[IoSlice]) -> (usize, SocketResult) {
11263 self.vectored_calls += 1;
11264 match self.vectored_script.pop_front() {
11265 Some((cap, status)) => {
11266 let offered: usize = bufs.iter().map(|slice| slice.len()).sum();
11267 (cap.min(offered), status)
11268 }
11269 None => self.stream.socket_write_vectored(bufs),
11270 }
11271 }
11272
11273 /// The override that makes this harness worth having.
11274 fn socket_wants_write(&self) -> bool {
11275 self.pending.get() > 0
11276 }
11277
11278 fn socket_ref(&self) -> &mio::net::TcpStream {
11279 &self.stream
11280 }
11281
11282 fn socket_mut(&mut self) -> &mut mio::net::TcpStream {
11283 &mut self.stream
11284 }
11285
11286 fn peer_addr(&self) -> Option<std::net::SocketAddr> {
11287 mio::net::TcpStream::peer_addr(&self.stream).ok()
11288 }
11289
11290 fn protocol(&self) -> crate::socket::TransportProtocol {
11291 crate::socket::TransportProtocol::Tls1_3
11292 }
11293
11294 fn read_error(&self) {}
11295
11296 fn write_error(&self) {}
11297 }
11298
11299 /// `state` is a parameter rather than a second copy of this fixture: the
11300 /// close decision reads it as `H2State::GoAway` and the write-pass
11301 /// finalization reads it as `H2State::Header`, and both need the same
11302 /// handler underneath.
11303 fn connection_with_backpressure(
11304 pool: &Rc<RefCell<Pool>>,
11305 pending: usize,
11306 drain_per_flush: usize,
11307 state: H2State,
11308 ) -> (H2Shell<BackpressuredTlsSocket>, std::net::TcpStream) {
11309 let (socket, peer) = connected_socket();
11310 let mut connection = H2Shell::new(
11311 Ulid::generate(),
11312 BackpressuredTlsSocket::new(socket, pending, drain_per_flush),
11313 Position::Server,
11314 &mut PoolBufferSource::new(Rc::downgrade(pool)),
11315 H2FloodConfig::default(),
11316 H2ConnectionConfig::default(),
11317 Duration::from_secs(30),
11318 None,
11319 Duration::from_secs(30),
11320 Some((H2StreamId::Zero, CLIENT_PREFACE_SIZE)),
11321 Ready::WRITABLE | Ready::HUP | Ready::ERROR,
11322 )
11323 .expect("a pool with free buffers must yield an H2 connection");
11324 connection.core.state = state;
11325 (connection, peer)
11326 }
11327
11328 /// Records that survive the flush hold the connection open.
11329 ///
11330 /// This is the branch whose own comment calls it the primary truncation
11331 /// vector: closing here sends FIN and destroys the records rustls is still
11332 /// holding, which the client reads as a truncated response.
11333 ///
11334 /// TO SEE THIS RED: delete the `self.ensure_tls_flushed(tls_wants_write);`
11335 /// call in the `CloseAction::ReArmAndContinue` arm of
11336 /// `ConnectionH2::dispatch_writable_state_after_flush`, which is where
11337 /// `H2Shell::writable`'s `H2State::GoAway` branch settles. The WRITABLE
11338 /// *event* bit is then never re-signalled and this test fails on its OWN
11339 /// assertion, `the WRITABLE event must be re-signalled so the event loop
11340 /// retries the flush`. The recipe deliberately does not swap
11341 /// `TlsFlushPhase::AfterFlush` for `BeforeFlush`: that reddens through the
11342 /// production `unreachable!`, which is someone else's assertion and would
11343 /// pass whatever this test claimed.
11344 #[test]
11345 fn a_flush_that_does_not_drain_keeps_the_connection_open() {
11346 let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
11347 // A kernel that accepts nothing: every flush leaves the records.
11348 let (mut connection, _peer) = connection_with_backpressure(&pool, 2, 0, H2State::GoAway);
11349 let mut context = test_context(&pool);
11350 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
11351
11352 // Premise: the handler really does report buffered records. Without
11353 // this the assertions below would pass against a handler taking the
11354 // trait's `false` default, which is exactly the blind spot #1454 names.
11355 assert!(
11356 connection.socket.socket_wants_write(),
11357 "premise: this harness must report buffered TLS records"
11358 );
11359
11360 let result = connection.writable(&mut context, EndpointClient(&mut router));
11361
11362 assert!(
11363 matches!(result, MuxResult::Continue),
11364 "records still buffered: the session must stay open, got {result:?}"
11365 );
11366 // `Continue` alone does not discriminate: `force_disconnect`'s server
11367 // arm also returns it for a live peer with records pending. The re-arm
11368 // path returns from the GoAway arm BEFORE reaching `force_disconnect`,
11369 // so the state is still `GoAway`; the fall-through would have gone
11370 // through `force_disconnect`, which sets `H2State::Error` first.
11371 assert!(
11372 matches!(connection.core.state, H2State::GoAway),
11373 "the GoAway arm must re-arm and return, not fall through to \
11374 force_disconnect, got {:?}",
11375 connection.core.state
11376 );
11377 // Not `readiness.interest`: that bit is this fixture's own argument to
11378 // `ConnectionH2::new` and nothing on this path clears it, so asserting
11379 // on it would assert on the harness. The edge-triggered re-arm is the
11380 // EVENT bit, set by `ensure_tls_flushed` -> `signal_pending_write`.
11381 assert!(
11382 connection.core.readiness.event.is_writable(),
11383 "the WRITABLE event must be re-signalled so the event loop retries \
11384 the flush, got {:?}",
11385 connection.core.readiness
11386 );
11387 // Not `socket_wants_write()`: with `drain_per_flush = 0` that can never
11388 // change, so it is a tautology of the harness no production edit can
11389 // falsify. The flush COUNT is falsifiable — a GoAway arm that skipped
11390 // its own flush would leave it at 1.
11391 assert!(
11392 connection.socket.flushes.get() >= 2,
11393 "the preamble and the GoAway arm must each attempt a flush, got {}",
11394 connection.socket.flushes.get()
11395 );
11396 }
11397
11398 /// A flush the kernel accepts closes within ONE `writable()` call.
11399 ///
11400 /// This is the tick-count assertion. The pre-image asked its second
11401 /// question inline, immediately after the flush; a split that returned
11402 /// `Continue` and waited to be called again would still deliver the bytes,
11403 /// but would double the latency of every close under backpressure and
11404 /// would strand the connection against a peer that never re-arms.
11405 ///
11406 /// TO SEE THIS RED: in `H2Shell::writable`, add
11407 /// `return MuxResult::Continue;` immediately after the
11408 /// `self.flush_tls_records();` inside the `H2WritableStateTarget::Flush`
11409 /// arm — the flush the `H2State::GoAway` branch asks for, performed and
11410 /// then deferred on. The first call then
11411 /// returns `Continue` and the final assertion fails with `a drained flush
11412 /// must reach the disconnect in the SAME writable call`. Measured: `1115
11413 /// passed; 2 failed`, the sibling being
11414 /// `a_flush_that_does_not_drain_keeps_the_connection_open`, which never
11415 /// reaches its own post-flush answer under the deferral either. Dropping
11416 /// the flush as well would redden the premise at the top of the test
11417 /// instead, with a different message.
11418 #[test]
11419 fn a_flush_that_succeeds_closes_within_one_writable_call() {
11420 let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
11421 // Two records, one drained per flush: the preamble takes one and the
11422 // GoAway arm's own flush takes the other, so the post-flush query is
11423 // the first one that can answer `false`.
11424 let (mut connection, _peer) = connection_with_backpressure(&pool, 2, 1, H2State::GoAway);
11425 let mut context = test_context(&pool);
11426 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
11427
11428 assert!(
11429 connection.socket.socket_wants_write(),
11430 "premise: this harness must report buffered TLS records"
11431 );
11432
11433 let result = connection.writable(&mut context, EndpointClient(&mut router));
11434
11435 assert!(
11436 !connection.socket.socket_wants_write(),
11437 "premise: both flushes landed, so nothing is pending any more"
11438 );
11439 assert!(
11440 connection.socket.flushes.get() >= 2,
11441 "the preamble and the GoAway arm must each attempt a flush, got {}",
11442 connection.socket.flushes.get()
11443 );
11444 assert!(
11445 !matches!(result, MuxResult::Continue),
11446 "a drained flush must reach the disconnect in the SAME writable \
11447 call, not defer it to another tick: got {result:?}"
11448 );
11449 }
11450
11451 /// A GoAway flush the kernel refuses keeps the connection open WITHOUT
11452 /// re-raising WRITABLE: the kernel owes the socket its next edge, and a
11453 /// synthetic one made `Mux::ready_inner` repeat the refused flush on
11454 /// every inner iteration until `MAX_LOOP_ITERATIONS`
11455 /// (sozu-proxy/sozu#1788). Once the kernel drains, the next writable
11456 /// pass flushes the records and reaches the disconnect, so no record is
11457 /// lost on the way to the close.
11458 ///
11459 /// TO SEE THIS RED: drop `&& !self.kernel_refused_write` from
11460 /// `ConnectionH2::ensure_tls_flushed`. The refused pass then re-raises
11461 /// the WRITABLE event.
11462 #[test]
11463 fn a_refused_goaway_flush_waits_for_the_kernel_edge_then_closes() {
11464 let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
11465 let (mut connection, _peer) = connection_with_backpressure(&pool, 2, 0, H2State::GoAway);
11466 connection.socket.refuse_flush = true;
11467 let mut context = test_context(&pool);
11468 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
11469 connection.core.readiness.event.insert(Ready::WRITABLE);
11470
11471 let result = connection.writable(&mut context, EndpointClient(&mut router));
11472
11473 assert!(
11474 matches!(result, MuxResult::Continue),
11475 "records still buffered: the session must stay open, got {result:?}"
11476 );
11477 assert!(
11478 matches!(connection.core.state, H2State::GoAway),
11479 "the GoAway arm must keep the connection, got {:?}",
11480 connection.core.state
11481 );
11482 assert!(
11483 connection.core.readiness.interest.is_writable(),
11484 "WRITABLE interest must stay so the kernel edge is delivered"
11485 );
11486 assert!(
11487 !connection.core.readiness.event.is_writable(),
11488 "a refused flush must leave WRITABLE to the kernel edge, got {:?}",
11489 connection.core.readiness
11490 );
11491
11492 // The peer reads: the kernel takes the records and raises its edge.
11493 connection.socket.drain_per_flush = 2;
11494 connection.core.readiness.event.insert(Ready::WRITABLE);
11495 let result = connection.writable(&mut context, EndpointClient(&mut router));
11496
11497 assert!(
11498 !connection.socket.socket_wants_write(),
11499 "the kernel edge must flush the pending records"
11500 );
11501 assert!(
11502 !matches!(result, MuxResult::Continue),
11503 "a drained GoAway flush must reach the disconnect, got {result:?}"
11504 );
11505 }
11506
11507 // ── The write pass's own flush triple (`finalize_write`) ────────────
11508 //
11509 // `ConnectionH2::finalize_write` ends every write pass with the same
11510 // query / flush / query shape the close sites use, and its decision now
11511 // lives beside theirs in `h2_close::finalize_action`. The two tests below
11512 // are the CALLER half — that `h2.rs` performs the step each answer names
11513 // and wires the inputs to the right parameters. The answers themselves are
11514 // enumerated exhaustively in `h2_close`'s own tables, which no socket can
11515 // reach: `SkipFlush`, `Parked` and `RetainPendingBack` are covered there
11516 // only, because reaching them through `writable()` needs a stream carrying
11517 // response bytes through the scheduler and none of this module's fixtures
11518 // builds one. Same boundary `h2_close`'s `force_disconnect` re-arm branch
11519 // already sits on.
11520
11521 /// The pass flushes once of its own and re-arms when records survive it.
11522 ///
11523 /// Three records, one drained per flush: `writable`'s preamble takes the
11524 /// first, `finalize_write`'s own flush takes the second, and the third is
11525 /// still pending when the post-flush query runs — so the re-arm branch is
11526 /// reached with the socket genuinely still holding data, not by a fixture
11527 /// that can only answer one way.
11528 ///
11529 /// TO SEE THIS RED, either half independently:
11530 /// (a) answer `H2FinalizeTarget::SkipFlush` from the
11531 /// `FinalizeAction::Flush` arm of `finalize_write` (the `SkipFlush`
11532 /// behaviour, which is what a caller that dropped the `socket_write`
11533 /// input would do for every pass). `write_streams` then performs no
11534 /// empty-buffer flush of its own and the flush count assertion fails
11535 /// with `finalize_write must attempt exactly one empty-buffer flush
11536 /// of its own`, `left: 1, right: 2`.
11537 /// (b) empty the `FinalizeAction::ReArm` arm of
11538 /// `finalize_write_after_flush`. The event assertion fails with `the
11539 /// WRITABLE event must be re-signalled`.
11540 /// Measured: `1114 passed; 1 failed` for each half, this test and nothing
11541 /// else. Neither recipe reddens through a production `unreachable!`.
11542 #[test]
11543 fn a_finalized_write_pass_flushes_once_and_re_arms_while_records_survive() {
11544 let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
11545 let (mut connection, _peer) = connection_with_backpressure(&pool, 3, 1, H2State::Header);
11546 let mut context = test_context(&pool);
11547 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
11548
11549 assert!(
11550 connection.socket.socket_wants_write(),
11551 "premise: this harness must report buffered TLS records"
11552 );
11553 // Not the interest bit: `signal_pending_write` touches `event` only,
11554 // and this fixture hands `Ready::WRITABLE` to `ConnectionH2::new` as
11555 // its INTEREST, so an assertion written against interest would be
11556 // reading the fixture's own argument back and could not see the
11557 // re-arm at all.
11558 assert!(
11559 !connection.core.readiness.event.is_writable(),
11560 "premise: the WRITABLE event must start clear so the re-arm below is the only thing that can set it, got {:?}",
11561 connection.core.readiness
11562 );
11563
11564 let result = connection.writable(&mut context, EndpointClient(&mut router));
11565
11566 assert!(
11567 matches!(result, MuxResult::Continue),
11568 "a write pass with records still buffered continues, got {result:?}"
11569 );
11570 assert_eq!(
11571 connection.socket.flushes.get(),
11572 2,
11573 "finalize_write must attempt exactly one empty-buffer flush of its own on top of the preamble's"
11574 );
11575 assert!(
11576 connection.socket.socket_wants_write(),
11577 "premise: one record must survive both flushes, or the post-flush query could only answer one way"
11578 );
11579 assert!(
11580 connection.core.readiness.event.is_writable(),
11581 "the WRITABLE event must be re-signalled so the event loop retries the flush, got {:?}",
11582 connection.core.readiness
11583 );
11584 }
11585
11586 /// A pass that owes nothing withdraws `Ready::WRITABLE` interest.
11587 ///
11588 /// This is the other side of the same decision: no TLS records (a plain
11589 /// `mio::net::TcpStream` takes `socket_wants_write()`'s `false` default),
11590 /// no parked `expect_write`, no bytes written, no queued control frame.
11591 /// Forward progress must come from an external trigger, so the connection
11592 /// relinquishes the bit rather than busy-spinning against the dispatcher.
11593 ///
11594 /// TO SEE THIS RED: delete `self.readiness.interest.remove(Ready::WRITABLE);`
11595 /// from the `FinalizeAction::Quiesce` arm of `finalize_write`, leaving its
11596 /// debug narration in place. The final assertion fails with `a pass that
11597 /// owes nothing must relinquish WRITABLE interest`. The interest bit is
11598 /// asserted here rather than the event bit because withdrawal is what this
11599 /// branch does, and it is set by the test rather than by the fixture —
11600 /// `test_h2_connection` arms `READABLE | HUP | ERROR` only.
11601 #[test]
11602 fn a_write_pass_that_owes_nothing_withdraws_writable_interest() {
11603 let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
11604 let (mut connection, _peer) = test_h2_connection(&pool, None);
11605 let mut context = test_context(&pool);
11606 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
11607
11608 connection.core.state = H2State::Header;
11609 connection.core.readiness.interest.insert(Ready::WRITABLE);
11610
11611 assert!(
11612 !connection.socket.socket_wants_write(),
11613 "premise: a plain TcpStream holds no TLS records, so the readiness policy is reached at all"
11614 );
11615 assert!(
11616 connection.core.stream_table.expect_write().is_none(),
11617 "premise: no parked partial write, or the policy is suppressed"
11618 );
11619
11620 let result = connection.writable(&mut context, EndpointClient(&mut router));
11621
11622 assert!(
11623 matches!(result, MuxResult::Continue),
11624 "an empty write pass continues, got {result:?}"
11625 );
11626 assert!(
11627 !connection.core.readiness.interest.is_writable(),
11628 "a pass that owes nothing must relinquish WRITABLE interest, got {:?}",
11629 connection.core.readiness
11630 );
11631 }
11632
11633 // ── The control-frame drains' own edge-triggered re-arm ─────────────
11634 //
11635 // `ConnectionH2::flush_pending_control_frames` queues the WINDOW_UPDATE
11636 // and RST_STREAM frames behind the ordered output and asks for ONE flush
11637 // (#1604). When that flush stalls, `update_readiness_after_write`
11638 // (`mux/mod.rs`) has just REMOVED the WRITABLE event bit, and the
11639 // `Stalled` answer puts it back when the socket still holds records it
11640 // could not hand to the kernel — unless the kernel refused the write
11641 // (`WouldBlock`): it then owes the socket its next edge, and the re-arm
11642 // would only repeat the refused write (sozu-proxy/sozu#1788,
11643 // `a_refused_goaway_flush_waits_for_the_kernel_edge_then_closes`). The
11644 // two tests below therefore stall the flush on a zero-length write the
11645 // kernel did NOT refuse, the case the re-arm still exists for. Before #1604 each of the two stages had a
11646 // stall tail of its own; the two tests below cover the one that remains.
11647 //
11648 // Until the two tests below existed, nothing in the suite reached either
11649 // stalled branch. Measured by replacing each re-arm with a `panic!` and
11650 // running the whole `sozu-lib` suite against `98745bba`: 1092 passed, 0
11651 // failed, neither panic observed. That is what these tests are for —
11652 // `Readiness::signal_pending_write`'s two `debug_assert!`s only guard a
11653 // path some test actually walks, and a stage that spells the re-arm by
11654 // hand carries no assertion at all.
11655 //
11656 // Neither test can see WHICH spelling a stage uses:
11657 // `readiness.event.insert(Ready::WRITABLE)` and `signal_pending_write()`
11658 // are indistinguishable from outside, by construction — `insert` cannot
11659 // clear a neighbouring bit, so the two are the same state transition.
11660 // What they detect is the re-arm going missing, which strands the
11661 // connection: the serialized frame waits in the output queue and no
11662 // further epoll edge is coming for it.
11663
11664 /// The WINDOW_UPDATE drain re-signals WRITABLE when its flush stalls.
11665 ///
11666 /// TO SEE THIS RED: in `ConnectionH2::handle_control_flush`, replace
11667 /// `H2ControlFlushTarget::Stalled` with
11668 /// `H2ControlFlushTarget::Done(MuxResult::Continue)` — the pass still
11669 /// ends on `Continue`, and the only thing lost is the TLS re-arm
11670 /// `H2Shell::writable` performs for `Stalled`. The last assertion fails
11671 /// with `the stalled WINDOW_UPDATE drain must re-signal the WRITABLE
11672 /// event`: `flush_output_to_socket` cleared the bit on the way in and
11673 /// nothing else on this path sets it. Measured 2026-09-27: this test and
11674 /// its RST_STREAM sibling fail, and no other test of the pair's filter.
11675 #[test]
11676 fn a_stalled_window_update_drain_re_signals_the_writable_event() {
11677 let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
11678 // One record rustls still holds and a kernel that accepts nothing, so
11679 // `socket_wants_write()` answers true for the whole test.
11680 let (mut connection, _peer) = connection_with_backpressure(&pool, 1, 0, H2State::Header);
11681 // The output flush stalls on its first round. A zero-length write
11682 // is what `update_readiness_after_write` reads as a stall, and it
11683 // removes the WRITABLE event bit before the drain stage can re-arm.
11684 // `Continue`, not `WouldBlock`: no kernel edge is owed for it.
11685 connection
11686 .socket
11687 .write_script
11688 .push_back((0, SocketResult::Continue));
11689 let mut context = test_context(&pool);
11690 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
11691
11692 assert!(
11693 connection.socket.socket_wants_write(),
11694 "premise: this harness must report buffered TLS records, or the \
11695 re-arm is gated off and the assertion below could not fail"
11696 );
11697 // Connection-level replenishment: ordinary housekeeping, queued the
11698 // way `handle_frame` queues it.
11699 connection.core.queue_window_update(0, 65_535);
11700 assert!(
11701 connection.core.stream_table.expect_write().is_none(),
11702 "premise: no parked partial write, or the drain stage is skipped"
11703 );
11704
11705 let result = connection.writable(&mut context, EndpointClient(&mut router));
11706
11707 assert!(
11708 matches!(result, MuxResult::Continue),
11709 "a stalled control-frame flush parks the pass and continues, got \
11710 {result:?}"
11711 );
11712 assert!(
11713 !connection.core.output.is_empty(),
11714 "premise: the pass must have taken the STALLED branch of the \
11715 output flush, which leaves the WINDOW_UPDATE queued"
11716 );
11717 assert!(
11718 connection.core.readiness.event.is_writable(),
11719 "the stalled WINDOW_UPDATE drain must re-signal the WRITABLE \
11720 event: under edge-triggered epoll the parked frame has no other \
11721 wake-up, got {:?}",
11722 connection.core.readiness
11723 );
11724 }
11725
11726 /// The RST_STREAM drain re-signals WRITABLE when its flush stalls.
11727 ///
11728 /// The sibling of the WINDOW_UPDATE test above, on the stage beneath it.
11729 /// No WINDOW_UPDATE is queued on purpose: that stage returns early, so
11730 /// queueing one would make this test exercise the wrong drain.
11731 ///
11732 /// TO SEE THIS RED: the `handle_control_flush` mutation described on the
11733 /// WINDOW_UPDATE test above. The last assertion fails with `the stalled
11734 /// RST_STREAM drain must re-signal the WRITABLE event`. Measured
11735 /// 2026-09-27.
11736 #[test]
11737 fn a_stalled_rst_stream_drain_re_signals_the_writable_event() {
11738 let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
11739 let (mut connection, _peer) = connection_with_backpressure(&pool, 1, 0, H2State::Header);
11740 connection
11741 .socket
11742 .write_script
11743 .push_back((0, SocketResult::Continue));
11744 let mut context = test_context(&pool);
11745 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
11746
11747 assert!(
11748 connection.socket.socket_wants_write(),
11749 "premise: this harness must report buffered TLS records, or the \
11750 re-arm is gated off and the assertion below could not fail"
11751 );
11752 // One RST, well under the pending-queue bound, so the overflow check
11753 // above this stage does not escalate to GOAWAY(ENHANCE_YOUR_CALM) and
11754 // swallow the drain.
11755 assert!(
11756 connection
11757 .core
11758 .enqueue_rst(1, H2Error::Cancel, RstOrigin::Local)
11759 .is_none(),
11760 "premise: a single RST must not trip the flood detector"
11761 );
11762 assert!(
11763 connection
11764 .core
11765 .flow_control
11766 .pending_window_updates_is_empty(),
11767 "premise: no queued WINDOW_UPDATE, or the stage above returns \
11768 first and this test exercises the wrong drain"
11769 );
11770
11771 let result = connection.writable(&mut context, EndpointClient(&mut router));
11772
11773 assert!(
11774 matches!(result, MuxResult::Continue),
11775 "a stalled control-frame flush parks the pass and continues, got \
11776 {result:?}"
11777 );
11778 assert!(
11779 !connection.core.output.is_empty(),
11780 "premise: the pass must have taken the STALLED branch of the \
11781 output flush, which leaves the RST_STREAM queued"
11782 );
11783 assert!(
11784 connection.core.readiness.event.is_writable(),
11785 "the stalled RST_STREAM drain must re-signal the WRITABLE event: \
11786 under edge-triggered epoll the parked frame has no other wake-up, \
11787 got {:?}",
11788 connection.core.readiness
11789 );
11790 }
11791
11792 /// The kernel-refused sibling of the two drain tests above: the output
11793 /// flush stalls on `(0, WouldBlock)`, and the `Stalled` arm must leave
11794 /// WRITABLE to the kernel's next edge instead of re-raising it. A
11795 /// synthetic edge here made `Mux::ready_inner` repeat the refused flush
11796 /// on every inner iteration (sozu-proxy/sozu#1788).
11797 ///
11798 /// TO SEE THIS RED: drop `self.note_write_status(status);` from
11799 /// `ConnectionH2::consume_output_flush`. The refusal is then never
11800 /// recorded and `ensure_tls_flushed` re-raises the event for both drains.
11801 #[test]
11802 fn a_refused_control_frame_drain_leaves_writable_to_the_kernel() {
11803 for drain in ["WINDOW_UPDATE", "RST_STREAM"] {
11804 let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
11805 let (mut connection, _peer) =
11806 connection_with_backpressure(&pool, 1, 0, H2State::Header);
11807 connection
11808 .socket
11809 .write_script
11810 .push_back((0, SocketResult::WouldBlock));
11811 let mut context = test_context(&pool);
11812 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
11813 if drain == "WINDOW_UPDATE" {
11814 connection.core.queue_window_update(0, 65_535);
11815 } else {
11816 assert!(
11817 connection
11818 .core
11819 .enqueue_rst(1, H2Error::Cancel, RstOrigin::Local)
11820 .is_none(),
11821 "premise: a single RST must not trip the flood detector"
11822 );
11823 }
11824 connection.core.readiness.event.insert(Ready::WRITABLE);
11825
11826 let result = connection.writable(&mut context, EndpointClient(&mut router));
11827
11828 assert!(
11829 matches!(result, MuxResult::Continue),
11830 "a refused {drain} drain parks the pass and continues, got {result:?}"
11831 );
11832 assert!(
11833 !connection.core.output.is_empty(),
11834 "premise: the {drain} flush must have stalled, leaving the frame queued"
11835 );
11836 assert!(
11837 connection.socket.socket_wants_write(),
11838 "premise: rustls must still hold records, or the re-arm is gated off"
11839 );
11840 assert!(
11841 !connection.core.readiness.event.is_writable(),
11842 "a {drain} drain the kernel refused must leave WRITABLE to the \
11843 kernel's next edge, got {:?}",
11844 connection.core.readiness
11845 );
11846 }
11847 }
11848
11849 /// A stream write the kernel refuses, while rustls still holds records,
11850 /// ends the pass without a synthetic WRITABLE. The pass already wrote
11851 /// through `socket_write_vectored`, so `finalize_write` skips its own
11852 /// flush and decides on the refusal `handle_write` recorded: the preamble
11853 /// flush before it answered `Continue`, so without that record the
11854 /// `ReArm` would re-raise the event.
11855 ///
11856 /// TO SEE THIS RED: drop `self.note_write_status(status);` from
11857 /// `ConnectionH2::handle_write`.
11858 #[test]
11859 fn a_refused_stream_write_leaves_writable_to_the_kernel() {
11860 let pool = make_pool_for_invariant_16();
11861 let (mut connection, mut context, gid, _peer) = writable_fixture(
11862 &pool,
11863 &[FIRST_BLOCK, SECOND_BLOCK],
11864 &[(0, SocketResult::WouldBlock)],
11865 );
11866 connection.socket.pending.set(1);
11867 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
11868 assert_prepare_gate_is_shut(&context, gid);
11869 connection.core.readiness.event.insert(Ready::WRITABLE);
11870
11871 let result = connection.writable(&mut context, EndpointClient(&mut router));
11872
11873 assert!(
11874 matches!(result, MuxResult::Continue),
11875 "a refused stream write continues, got {result:?}"
11876 );
11877 assert_eq!(
11878 connection.socket.vectored_calls, 1,
11879 "premise: the pass must have reached the stream write"
11880 );
11881 assert!(
11882 connection.socket.socket_wants_write(),
11883 "premise: rustls must still hold records"
11884 );
11885 assert!(
11886 !connection.core.readiness.event.is_writable(),
11887 "a stream write the kernel refused must leave WRITABLE to the \
11888 kernel's next edge, got {:?}",
11889 connection.core.readiness
11890 );
11891 }
11892
11893 // ── The vectored write loop: a partial write that reports WouldBlock ──
11894 //
11895 // First, which shape each neighbour targets, because #1454 asks for that
11896 // and the shapes are near-identical. `h2.rs` has FOUR `socket_write(&[])`
11897 // sites. Three are the `query / socket_write(&[]) / query` TRIPLE, where
11898 // the middle call discards both `size` and `status` and only the second
11899 // query says whether the flush landed:
11900 //
11901 // - `finalize_write`, the BeforeFlush/AfterFlush pair — covered by
11902 // `a_finalized_write_pass_flushes_once_and_re_arms_while_records_survive`.
11903 // - the `H2State::GoAway` arm of `dispatch_writable_state`, whose flush
11904 // `writable` performs for it — covered by
11905 // `a_flush_that_does_not_drain_keeps_the_connection_open` and
11906 // `a_flush_that_succeeds_closes_within_one_writable_call`.
11907 // - `writable`'s preamble paired with the `error_close_action` query of
11908 // `dispatch_writable_state`'s `(H2State::Error, Position::Server)`
11909 // arm. The preamble half is asserted by the two GoAway tests above,
11910 // and #1454's
11911 // `a_rustls_frontend_in_error_state_stays_open_until_its_records_drain`
11912 // drives the second query over the production handler — this line
11913 // said **Uncovered** until that test existed, and it was already
11914 // wrong before this changeset renamed what it points at.
11915 //
11916 // The fourth, in `flush_output_buffer`, is NOT a triple: it keeps the
11917 // `status` the flush returned instead of re-querying. Also uncovered.
11918 //
11919 // The tests below target none of those. They target **the vectored loop
11920 // the triples bracket** — the pre-image's `while !kawa.out.is_empty()`,
11921 // now `poll_write_target`'s round-again over `H2WritePhase::Flush` — so
11922 // naming a triple for them would be naming the wrong shape.
11923 //
11924 // What had never run against production code: a `socket_write_vectored`
11925 // that moves bytes AND reports `WouldBlock`. `update_readiness` (`mux/mod.rs`)
11926 // classifies a pass as stalled **iff `size == 0`** — `status` only clears
11927 // the event bit — so `size > 0 && status == WouldBlock` is NOT a stall and
11928 // the loop must go round again. A write machine that treated
11929 // `status != Continue` as a pass terminator would drop that second write
11930 // and truncate the response, and would pass every test that existed.
11931 //
11932 // Why no test could see it. `BackpressuredTlsSocket::socket_write_vectored`
11933 // delegated to a real loopback socket, and a kernel that blocks answers
11934 // `(0, WouldBlock)` — never `(size > 0, WouldBlock)`. Only `FrontRustls`
11935 // returns that pair, and it does so structurally: `buffered_size` counts
11936 // plaintext `session.writer().write(..)` accepted, while `can_write` goes
11937 // false when `write_tls` blocks against the kernel. Two independent
11938 // quantities, one return value.
11939 //
11940 // `h2_transmit`'s `qc_partial_writes_preserve_the_byte_stream` does drive
11941 // partial writes, but through `drive`, a loop written in its own test
11942 // module that calls neither `ConnectionH2::handle_write` nor
11943 // `update_readiness_after_write`; its `WritePlan` carries an accept COUNT
11944 // and no `SocketResult` at all. A mirror cannot constrain the thing it
11945 // mirrors, and that plan cannot express this pair in the first place.
11946
11947 /// What the pre-image's `FlushOutcome` said about one stream's flush,
11948 /// re-derived here because production no longer has the type.
11949 ///
11950 /// The inversion deleted it along with `flush_stream_out`: the flush loop
11951 /// is now the `Transmit`/`handle_write` round trip, whose exit condition
11952 /// is read by `poll_write_target` from `kawa.out` and `pass.stalled`
11953 /// rather than reported as a return value. `Drained` is therefore derived
11954 /// from the queue being empty — the pre-image's own
11955 /// `while !kawa.out.is_empty()` exit condition, verbatim — and NOT from
11956 /// `expect_write` being parked. The distinction matters: the park has its
11957 /// own dedicated tests below, and deriving the outcome from it would make
11958 /// this classification move under edits those tests already cover.
11959 ///
11960 /// **It is therefore the same fact as the `out_is_empty` element of
11961 /// [`drive_flush_stream_out`]'s tuple, and the three tests below assert
11962 /// both.** Stated rather than removed: their bodies are unchanged from
11963 /// before the inversion, which is what makes them evidence that spans it,
11964 /// and the alternatives were measured worse. Deriving this from
11965 /// `expect_write` would couple it to the park, which the stall tests
11966 /// already pin — the double-assertion would become a second test reddening
11967 /// under the `set_expect_write` recipe. Deriving it from the WRITABLE
11968 /// event bit cannot work either: `update_readiness` clears that bit on
11969 /// every non-`Continue` round, so a drained two-round pass and a stalled
11970 /// one leave it identical.
11971 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
11972 enum FlushOutcome {
11973 /// All queued bytes were drained to the socket.
11974 Drained,
11975 /// The socket blocked before the queue was drained.
11976 Stalled,
11977 }
11978
11979 /// Drive one write pass over `blocks` with `script` as the socket's
11980 /// answers, returning what the flush did.
11981 ///
11982 /// Retargeted by the poll/handle inversion. There is no associated
11983 /// function left to call: the loop this drives is spread across
11984 /// `poll_write_target`'s round-again, the shell's gather / write / confirm
11985 /// and `handle_write`, and the smallest production seam that contains all
11986 /// three is `write_streams`. It is called directly rather than through
11987 /// `writable()` so that the preamble's pending-control-frame flush and TLS
11988 /// flush stay out of the measurement; `pending = 0` keeps
11989 /// `socket_wants_write()` false for the whole pass, and nothing below
11990 /// asserts on time, so skipping `writable`'s `self.now = context.now` is
11991 /// inert.
11992 ///
11993 /// What is unchanged is the point of the harness: `vectored_script` still
11994 /// lets the `(size, status)` pair be CHOSEN rather than negotiated with a
11995 /// kernel, which is the only way `(size > 0, WouldBlock)` is expressible
11996 /// at all.
11997 fn drive_flush_stream_out(
11998 pool: &Rc<RefCell<Pool>>,
11999 blocks: &[&'static [u8]],
12000 script: &[(usize, SocketResult)],
12001 ) -> (FlushOutcome, usize, usize, bool, Readiness) {
12002 let (mut connection, mut context, gid, _peer) = writable_fixture(pool, blocks, script);
12003 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
12004
12005 connection.write_streams(&mut context, EndpointClient(&mut router));
12006
12007 let out_is_empty = context.streams[gid].back.out.is_empty();
12008 (
12009 if out_is_empty {
12010 FlushOutcome::Drained
12011 } else {
12012 FlushOutcome::Stalled
12013 },
12014 context.streams[gid].metrics.bout,
12015 connection.socket.vectored_calls,
12016 out_is_empty,
12017 connection.core.readiness.clone(),
12018 )
12019 }
12020
12021 /// Bytes the two blocks below carry, and the whole response as far as
12022 /// this stream is concerned: ONE DATA frame on stream 1, split across two
12023 /// blocks, 22 + 21 bytes. A stream's `kawa.out` only ever holds whole H2
12024 /// frames, and since #1604 the write pass relies on it to find where the
12025 /// frame a partial write cut ends (`h2_transmit::frame_tail`).
12026 const FIRST_BLOCK: &[u8] = b"\x00\x00\x22\x00\x00\x00\x00\x00\x01HTTP/2 respon";
12027 const SECOND_BLOCK: &[u8] = b" and its continuation";
12028 const TOTAL_QUEUED: usize = FIRST_BLOCK.len() + SECOND_BLOCK.len();
12029
12030 /// A write that moved bytes but reported `WouldBlock` does NOT end the
12031 /// pass: the loop goes round again and delivers the rest.
12032 ///
12033 /// This is the truncation vector of #1454 at the vectored loop — not at
12034 /// any of the three `socket_wants_write()` triples. `FrontRustls` returns
12035 /// `(buffered_size, WouldBlock)` with `buffered_size > 0` whenever rustls
12036 /// took plaintext off our hands while the kernel was full, and
12037 /// `update_readiness` deliberately calls that NOT stalled. A machine that
12038 /// terminated the pass on `status != Continue` would leave
12039 /// `SECOND_BLOCK` queued and send the response short.
12040 ///
12041 /// TO SEE THIS RED: in `handle_write`, change
12042 /// `pass.stalled = update_readiness_after_write(size, status, &mut self.readiness);`
12043 /// to
12044 /// `pass.stalled = update_readiness_after_write(size, status, &mut self.readiness)
12045 /// || !matches!(status, SocketResult::Continue);`
12046 /// — the pass-terminator shape the `write_streams` inversion could
12047 /// introduce, and the reason `SocketResult` is confined to this one
12048 /// statement and this one parameter. Measured on that mutation: this test
12049 /// fails on its own first assertion, `a vectored write that moved bytes
12050 /// but reported WouldBlock must not end the pass`, with
12051 /// `left: 5, right: 43`. It is one of THREE failures, the siblings being
12052 /// `a_partial_write_reporting_would_block_continues_the_pass_through_writable`
12053 /// and `a_yielded_incremental_stream_is_prepared_once_per_pass`; every
12054 /// other test in the suite passes, including both controls below. No
12055 /// production `debug_assert!` fires: every scripted size is clamped to the
12056 /// gather's offer.
12057 #[test]
12058 fn a_partial_write_reporting_would_block_continues_the_pass() {
12059 let pool = make_pool_for_invariant_16();
12060 // Round 1 accepts 5 of the 43 offered and reports WouldBlock; round 2
12061 // takes everything still queued and reports Continue.
12062 // The event bit is deliberately not read here: `update_readiness`
12063 // removes WRITABLE on the WouldBlock round and the Continue round does
12064 // not put it back, so its final value is identical under the correct
12065 // and the broken machine. An assertion on it could not fail.
12066 let (outcome, bytes_written, vectored_calls, out_is_empty, _readiness) =
12067 drive_flush_stream_out(
12068 &pool,
12069 &[FIRST_BLOCK, SECOND_BLOCK],
12070 &[
12071 (5, SocketResult::WouldBlock),
12072 (TOTAL_QUEUED, SocketResult::Continue),
12073 ],
12074 );
12075
12076 assert_eq!(
12077 bytes_written, TOTAL_QUEUED,
12078 "a vectored write that moved bytes but reported WouldBlock must \
12079 not end the pass: the loop must go round again and deliver the \
12080 remaining bytes, or the response is truncated"
12081 );
12082 assert_eq!(
12083 vectored_calls, 2,
12084 "the loop must issue a SECOND socket_write_vectored after the \
12085 partial write, not settle for the first"
12086 );
12087 assert!(
12088 out_is_empty,
12089 "every queued block must be consumed once the pass drains"
12090 );
12091 assert!(
12092 matches!(outcome, FlushOutcome::Drained),
12093 "a queue emptied within the pass is Drained, got {outcome:?}"
12094 );
12095 }
12096
12097 /// A write the socket takes whole, in one round, reporting `Continue`.
12098 ///
12099 /// **This test does not discriminate, and that is its job.** It is the
12100 /// `status == Continue` path — the one every existing test already takes,
12101 /// because a real loopback socket with room answers exactly this. It
12102 /// passes against the correct loop AND against the pass-terminator
12103 /// mutation in the test above, since a terminator keyed on
12104 /// `status != Continue` never fires here. Kept as the standing
12105 /// demonstration that coverage of this shape proves nothing about the
12106 /// truncation vector: the suite was full of it and #1454 was open anyway.
12107 #[test]
12108 fn a_fully_accepted_write_drains_in_one_round() {
12109 let pool = make_pool_for_invariant_16();
12110 let (outcome, bytes_written, vectored_calls, out_is_empty, _readiness) =
12111 drive_flush_stream_out(
12112 &pool,
12113 &[FIRST_BLOCK, SECOND_BLOCK],
12114 &[(TOTAL_QUEUED, SocketResult::Continue)],
12115 );
12116
12117 assert_eq!(
12118 bytes_written, TOTAL_QUEUED,
12119 "a socket that takes the whole offer delivers the whole queue"
12120 );
12121 assert_eq!(
12122 vectored_calls, 1,
12123 "nothing is left to write, so the loop must not go round again"
12124 );
12125 assert!(out_is_empty, "the queue must be empty after a full accept");
12126 assert!(
12127 matches!(outcome, FlushOutcome::Drained),
12128 "a fully accepted offer is Drained, got {outcome:?}"
12129 );
12130 }
12131
12132 /// A write that moves NO bytes stalls the pass and drops WRITABLE.
12133 ///
12134 /// The other side of `update_readiness`'s single decision: `size == 0` is
12135 /// the stall, whatever the status. Together with the partial-write test
12136 /// above this pins that the discriminator is the SIZE and not the status —
12137 /// both rounds there reported a non-`Continue` status, and only this one
12138 /// stops.
12139 ///
12140 /// Like `a_fully_accepted_write_drains_in_one_round`, this passes under
12141 /// the pass-terminator mutation too: `(0, WouldBlock)` stalls either way.
12142 /// The size arm is the covered one; the partial arm was not.
12143 ///
12144 /// TO SEE THIS RED: in `update_readiness` (`mux/mod.rs`), change the
12145 /// `else` arm's trailing `true` to `false`, so no size ever reports a
12146 /// stall. Measured: the first assertion fails with `a write that moved no
12147 /// bytes must stall the pass, got Drained`. It does not hang — the script
12148 /// here is a single round, so the loop re-enters, finds the script spent,
12149 /// delegates to the live loopback socket and drains.
12150 #[test]
12151 fn a_write_that_moves_no_bytes_stalls_the_pass() {
12152 let pool = make_pool_for_invariant_16();
12153 let (outcome, bytes_written, vectored_calls, out_is_empty, readiness) =
12154 drive_flush_stream_out(
12155 &pool,
12156 &[FIRST_BLOCK, SECOND_BLOCK],
12157 &[(0, SocketResult::WouldBlock)],
12158 );
12159
12160 assert!(
12161 matches!(outcome, FlushOutcome::Stalled),
12162 "a write that moved no bytes must stall the pass, got {outcome:?}"
12163 );
12164 assert_eq!(
12165 vectored_calls, 1,
12166 "a stalled pass must not retry the socket within the same pass"
12167 );
12168 assert_eq!(bytes_written, 0, "a stalled write delivered nothing");
12169 assert!(
12170 !out_is_empty,
12171 "the undelivered blocks must stay queued for the next pass, or \
12172 they are lost"
12173 );
12174 assert!(
12175 !readiness.event.is_writable(),
12176 "a stalled pass must clear the WRITABLE event so the pass is not \
12177 re-entered before the socket says it can take more, got {readiness:?}"
12178 );
12179 }
12180
12181 // ── The two write-path sites `writable()` had never reached ──────────
12182 //
12183 // A stalled flush has ONE producer — the `pass.stalled` assignment in
12184 // `handle_write`, `FlushOutcome::Stalled` before the inversion — and
12185 // exactly two consumers, and both consumers were dead code as far as the
12186 // suite was concerned:
12187 //
12188 // - the resume path's `H2WriteTarget::Done(MuxResult::Continue)`, taken
12189 // in `H2WritePhase::Resume` for the stream `expect_write` parked, whose
12190 // transmits carry `debug_site = 2`;
12191 // - `H2WritePhase::Flush`'s
12192 // `set_expect_write(Some(H2StreamId::Other { .. }))` followed by the
12193 // move to `H2WritePhase::End` — `break 'outer` before the inversion.
12194 //
12195 // Measured on `38fe5851`, against the linear pre-image these tests were
12196 // written for: a `panic!` planted at BOTH consumers at once still leaves
12197 // the whole suite green — neither is reached by any test. A `panic!` on
12198 // the `return FlushOutcome::Stalled` they both read fails exactly ONE
12199 // test, `a_write_that_moves_no_bytes_stalls_the_pass` above, which called
12200 // `flush_stream_out` directly and never entered `write_streams` at all.
12201 // A `panic!` on the first line of the resume block leaves the suite green
12202 // as well: `debug_site = 2` had no caller in it. Those measurements are a
12203 // record of the revision named, not a claim about this one — the three
12204 // tests above now reach `write_streams` too, since `flush_stream_out` is
12205 // gone and there is no associated function left to call.
12206 //
12207 // No absolute pass count appears in this block or in any recipe below, on
12208 // purpose. A count of the whole `sozu-lib` suite is falsified by a test
12209 // added anywhere in the crate, and nothing checks it —
12210 // `check_doc_citations.py` reads only `doc/` and `**/LIFECYCLE.md`. The
12211 // failing test NAMES and the `left:`/`right:` values are what survive.
12212 //
12213 // Why the gap existed. A stall needs `size == 0` against a non-empty
12214 // `kawa.out`. The two `BackpressuredTlsSocket` `writable()` tests above
12215 // sit in `H2State::GoAway` and take that arm of `writable`, never
12216 // `write_streams`; the two after them do reach `write_streams`, but with
12217 // an empty stream table, so the scheduler loop visits no stream. Every
12218 // other `writable()` test writes to a live loopback socket with room, and
12219 // a kernel with room never answers 0.
12220 //
12221 // The missing step was never the socket — `vectored_script` already
12222 // scripts any `(size, status)` pair, including ones no kernel produces.
12223 // It was REGISTERING a stream that carries response bytes, so the
12224 // scheduler reaches it through `writable()`. That is what
12225 // `writable_fixture` below adds. It is now what `drive_flush_stream_out`
12226 // builds on too: the difference left between the two is only that this
12227 // group enters through `writable()` and that one through `write_streams`
12228 // directly.
12229
12230 /// The stream id every test below registers. Odd, because a server's peer
12231 /// opens odd-numbered streams (RFC 9113 §5.1.1).
12232 const REGISTERED_STREAM_ID: StreamId = 1;
12233
12234 /// A server connection in `H2State::Header` plus a context holding ONE
12235 /// registered stream whose response buffer already carries `blocks`, with
12236 /// the socket's `(size, status)` answers scripted.
12237 ///
12238 /// `pending = 0` keeps `socket_wants_write()` false for the whole pass, so
12239 /// `writable`'s preamble flush and both of `finalize_write`'s TLS branches
12240 /// stay out of the way: the readiness bits these tests read are decided by
12241 /// the write pass alone.
12242 ///
12243 /// The script is assigned to the field directly: a builder taking `self`
12244 /// by value cannot be applied to a handler already moved into
12245 /// `ConnectionH2::new`.
12246 ///
12247 /// The peer end of the loopback pair is returned for the caller to hold:
12248 /// dropping it would close the connection under the socket being tested.
12249 fn writable_fixture(
12250 pool: &Rc<RefCell<Pool>>,
12251 blocks: &[&'static [u8]],
12252 script: &[(usize, SocketResult)],
12253 ) -> (
12254 H2Shell<BackpressuredTlsSocket>,
12255 Context<TestListener>,
12256 GlobalStreamId,
12257 std::net::TcpStream,
12258 ) {
12259 let (mut connection, peer) = connection_with_backpressure(pool, 0, 0, H2State::Header);
12260 connection.socket.vectored_script = script.iter().copied().collect();
12261 let mut context = test_context(pool);
12262 let gid = context
12263 .create_stream(Ulid::generate(), 1 << 16)
12264 .expect("test context must create a stream");
12265 connection
12266 .core
12267 .stream_table
12268 .register(REGISTERED_STREAM_ID, gid, connection.core.now);
12269 for block in blocks {
12270 context.streams[gid]
12271 .back
12272 .out
12273 .push_back(kawa::OutBlock::Store(kawa::Store::Static(block)));
12274 }
12275 (connection, context, gid, peer)
12276 }
12277
12278 /// A fresh `Stream`'s response buffer is in `ParsingPhase::StatusLine`,
12279 /// which `is_main_phase()` answers `false` for, so the `write_streams`
12280 /// prepare gate stays shut and the pass is the flush and nothing else.
12281 ///
12282 /// Every `writable_fixture` test rests on that, because each one counts
12283 /// either bytes or socket calls and a prepare would add both. Asserted as
12284 /// a premise rather than assumed: if a future `Stream::new` started life
12285 /// in `Body`, those tests would keep passing while measuring something
12286 /// else. `a_yielded_incremental_stream_is_prepared_once_per_pass` below
12287 /// deliberately opens the gate instead, and builds its own fixture.
12288 fn assert_prepare_gate_is_shut(context: &Context<TestListener>, gid: GlobalStreamId) {
12289 let back = &context.streams[gid].back;
12290 assert!(
12291 !back.is_main_phase() && !back.is_terminated() && !back.is_error(),
12292 "premise: the prepare gate must stay shut so this pass is a pure \
12293 flush, got parsing_phase {:?}",
12294 back.parsing_phase
12295 );
12296 }
12297
12298 /// A write that moved bytes but reported `WouldBlock` does not end the
12299 /// pass — driven through `writable()`, over a registered stream.
12300 ///
12301 /// `a_partial_write_reporting_would_block_continues_the_pass` above pins
12302 /// the same contract one layer down, entering at `write_streams` and
12303 /// reading the flush's own outcome. This one pins that the SCHEDULER
12304 /// honours it: the `(size > 0, WouldBlock)` answer has to travel back out
12305 /// through `poll_write_target`'s round-again at `H2WritePhase::Flush`,
12306 /// past the `set_expect_write` park, and through `finalize_write` without
12307 /// anything on the way deciding the pass is over. A write machine that
12308 /// treated `status != Continue` as a pass terminator would park
12309 /// `expect_write`, leave `SECOND_BLOCK` queued, and truncate the response.
12310 ///
12311 /// TO SEE THIS RED: in `handle_write`, change
12312 /// `pass.stalled = update_readiness_after_write(size, status, &mut self.readiness);`
12313 /// to
12314 /// `pass.stalled = update_readiness_after_write(size, status, &mut self.readiness)
12315 /// || !matches!(status, SocketResult::Continue);`
12316 /// Measured: this test fails on its own first assertion, `every queued
12317 /// byte must reach the socket within the pass`, with `left: 5, right: 43`.
12318 /// That edit also reddens two siblings, so THREE tests fail and not one:
12319 /// `a_partial_write_reporting_would_block_continues_the_pass`, the same
12320 /// contract on the direct path, documenting the same recipe (`left: 5,
12321 /// right: 43`); and
12322 /// `a_yielded_incremental_stream_is_prepared_once_per_pass` below, whose
12323 /// first stream stalls on its `WouldBlock` round so the pass goes to
12324 /// `H2WritePhase::End` before the second stream is ever prepared
12325 /// (`left: 16384, right: 32768`). Three witnesses to one truncation vector, at three different
12326 /// depths — not a duplicate. No production `debug_assert!` fires: every
12327 /// scripted size is clamped to the gather's offer.
12328 #[test]
12329 fn a_partial_write_reporting_would_block_continues_the_pass_through_writable() {
12330 let pool = make_pool_for_invariant_16();
12331 let (mut connection, mut context, gid, _peer) = writable_fixture(
12332 &pool,
12333 &[FIRST_BLOCK, SECOND_BLOCK],
12334 &[
12335 (5, SocketResult::WouldBlock),
12336 (TOTAL_QUEUED, SocketResult::Continue),
12337 ],
12338 );
12339 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
12340
12341 assert_eq!(
12342 connection.core.stream_table.expect_write(),
12343 None,
12344 "premise: no parked write, so the pass takes the main loop and not \
12345 the resume path"
12346 );
12347 assert_prepare_gate_is_shut(&context, gid);
12348
12349 let result = connection.writable(&mut context, EndpointClient(&mut router));
12350
12351 assert_eq!(
12352 context.streams[gid].metrics.bout, TOTAL_QUEUED,
12353 "every queued byte must reach the socket within the pass: a write \
12354 that moved bytes under WouldBlock is not a stall, so the loop must \
12355 go round again instead of ending the pass short"
12356 );
12357 assert_eq!(
12358 connection.socket.vectored_calls, 2,
12359 "the pass must issue a SECOND socket_write_vectored after the \
12360 partial write, not settle for the first"
12361 );
12362 assert!(
12363 context.streams[gid].back.out.is_empty(),
12364 "every queued block must be consumed once the pass drains"
12365 );
12366 assert_eq!(
12367 connection.core.stream_table.expect_write(),
12368 None,
12369 "a pass that drained must not park expect_write: parking here \
12370 would defer a stream that owes nothing to the next writable tick"
12371 );
12372 assert!(
12373 matches!(result, MuxResult::Continue),
12374 "a drained write pass continues, got {result:?}"
12375 );
12376 }
12377
12378 /// A write pass that transmits allocates nothing once the connection is
12379 /// warm: the vectored descriptors live in `H2Shell::io_slices`, which
12380 /// keeps its capacity from one pass to the next instead of being built
12381 /// and freed by every `write_streams` call.
12382 ///
12383 /// The first pass is the warm-up and may grow the scratch; the second
12384 /// queues the same two blocks again and is the measurement. Both go
12385 /// through the loopback socket (empty script), so the write is a real
12386 /// `writev(2)` and the count covers the whole gather / write / confirm
12387 /// round trip plus `poll_write_target` and `handle_write` around it.
12388 ///
12389 /// TO SEE THIS RED: in `H2Shell::write_streams`, declare
12390 /// `let mut io_slices: Vec<IoSlice<'static>> = Vec::new();` above the
12391 /// loop and hand it to `gather`, the write and `confirm` in place of
12392 /// `self.io_slices`. Measured: this test fails with
12393 /// `left: 2, right: 1` under `cargo test`, and `left: 1, right: 0`
12394 /// under `cargo test --release` — one allocation per transmitting pass.
12395 #[test]
12396 fn a_warm_write_pass_allocates_nothing() {
12397 let pool = make_pool_for_invariant_16();
12398 let (mut connection, mut context, gid, _peer) =
12399 writable_fixture(&pool, &[FIRST_BLOCK, SECOND_BLOCK], &[]);
12400 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
12401 assert_prepare_gate_is_shut(&context, gid);
12402
12403 connection.write_streams(&mut context, EndpointClient(&mut router));
12404 assert!(
12405 context.streams[gid].back.out.is_empty(),
12406 "premise: the warm-up pass must drain, or the measured pass would \
12407 start from a different state"
12408 );
12409 for block in [FIRST_BLOCK, SECOND_BLOCK] {
12410 context.streams[gid]
12411 .back
12412 .out
12413 .push_back(kawa::OutBlock::Store(kawa::Store::Static(block)));
12414 }
12415 let calls_before = connection.socket.vectored_calls;
12416
12417 let before = crate::test_allocations::allocations();
12418 connection.write_streams(&mut context, EndpointClient(&mut router));
12419 let allocations = crate::test_allocations::allocations() - before;
12420
12421 assert!(
12422 connection.socket.vectored_calls > calls_before,
12423 "premise: the measured pass must reach socket_write_vectored"
12424 );
12425 assert!(
12426 context.streams[gid].back.out.is_empty(),
12427 "premise: the measured pass must drain the stream"
12428 );
12429 // `finalize_write`'s `Quiesce` arm formats a `DebugEvent::Str` under
12430 // `#[cfg(debug_assertions)]` only; a release build does not compile
12431 // it. Everything else the pass does must be allocation-free.
12432 let debug_instrumentation = usize::from(cfg!(debug_assertions));
12433 assert_eq!(
12434 allocations, debug_instrumentation,
12435 "a warm transmitting write pass must not allocate"
12436 );
12437 }
12438
12439 /// A zero-byte write parks `expect_write` on the stalled stream and ends
12440 /// the pass, so the next tick resumes exactly it.
12441 ///
12442 /// This is `poll_write_target`'s stall consumer — the
12443 /// `set_expect_write(Some(write_stream));` plus the move to
12444 /// `H2WritePhase::End` in the `H2WritePhase::Flush` arm, `break 'outer`
12445 /// before the inversion. Without the park, the next `writable()` would
12446 /// re-enter the scheduler from the top and re-run the priority ordering
12447 /// for a stream that is merely waiting on the socket, and `finalize_write`
12448 /// would read `expect_write_parked == false` and fall through to
12449 /// `Quiesce`, stripping `Ready::WRITABLE` from a connection that has bytes
12450 /// to deliver.
12451 ///
12452 /// TO SEE THIS RED: delete the
12453 /// `self.stream_table.set_expect_write(Some(write_stream));` statement
12454 /// from that arm, keeping the `pass.phase = H2WritePhase::End;` that
12455 /// follows it. Measured: this test fails on its own first
12456 /// assertion, `a stalled stream must park expect_write so the next pass
12457 /// resumes it`, with `left: None, right: Some(Other { id: 1, gid: 0 })`,
12458 /// and it is the ONLY failure in the suite.
12459 #[test]
12460 fn a_stalled_stream_parks_expect_write_for_the_next_pass() {
12461 let pool = make_pool_for_invariant_16();
12462 let (mut connection, mut context, gid, _peer) = writable_fixture(
12463 &pool,
12464 &[FIRST_BLOCK, SECOND_BLOCK],
12465 &[(0, SocketResult::WouldBlock)],
12466 );
12467 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
12468
12469 assert_eq!(
12470 connection.core.stream_table.expect_write(),
12471 None,
12472 "premise: nothing parked on entry, so any park below is this \
12473 pass's doing"
12474 );
12475 assert_prepare_gate_is_shut(&context, gid);
12476
12477 let result = connection.writable(&mut context, EndpointClient(&mut router));
12478
12479 assert_eq!(
12480 connection.core.stream_table.expect_write(),
12481 Some(H2StreamId::Other {
12482 id: REGISTERED_STREAM_ID,
12483 gid
12484 }),
12485 "a stalled stream must park expect_write so the next pass resumes \
12486 it instead of re-running the scheduler over a socket-blocked stream"
12487 );
12488 assert!(
12489 !context.streams[gid].back.out.is_empty(),
12490 "the undelivered blocks must stay queued for the next pass, or \
12491 they are lost"
12492 );
12493 assert_eq!(
12494 connection.socket.vectored_calls, 1,
12495 "a stalled pass must not retry the socket within the same pass"
12496 );
12497 assert_eq!(
12498 context.streams[gid].metrics.bout, 0,
12499 "a stalled write delivered nothing"
12500 );
12501 assert!(
12502 connection.core.readiness.interest.is_writable(),
12503 "a parked pass leaves every readiness bit alone — stripping \
12504 WRITABLE here would strand the parked stream, got {:?}",
12505 connection.core.readiness
12506 );
12507 assert!(
12508 matches!(result, MuxResult::Continue),
12509 "a parked write pass continues, got {result:?}"
12510 );
12511 }
12512
12513 /// A stalled resume returns before the scheduler pass begins.
12514 ///
12515 /// This is the OTHER stall consumer — `H2WritePhase::Resume`'s
12516 /// `if pass.stalled { return H2WriteTarget::Done(MuxResult::Continue); }`.
12517 /// The park it leaves standing is the point: falling through would clear
12518 /// `expect_write` on a stream the socket has just refused, and then run
12519 /// the whole priority ordering, converter pass and census for a
12520 /// connection whose socket is known to be full.
12521 ///
12522 /// The stream is registered even though the resume path reads its gid
12523 /// straight out of `expect_write` and needs no registration. That is what
12524 /// makes `vectored_calls` a witness: with the stream in the table, a pass
12525 /// that fell through into the scheduler phases would reach it again and
12526 /// issue a second `socket_write_vectored`.
12527 ///
12528 /// TO SEE THIS RED: delete the
12529 /// `return H2WriteTarget::Done(MuxResult::Continue);` from that arm,
12530 /// leaving the `if pass.stalled {}` with an empty body so control falls
12531 /// through to `self.stream_table.set_expect_write(None);`. Measured: this test fails
12532 /// on its own first assertion, `a stalled resume must leave expect_write
12533 /// parked`, with `left: None, right: Some(Other { id: 1, gid: 0 })`, and
12534 /// it is the ONLY failure in the suite. The
12535 /// second assertion moves under the same edit (`left: 2, right: 1`), the
12536 /// extra call being the main loop re-flushing the same stream through the
12537 /// delegated loopback socket once the one-entry script is spent.
12538 #[test]
12539 fn a_stalled_resume_does_not_begin_a_scheduler_pass() {
12540 let pool = make_pool_for_invariant_16();
12541 let (mut connection, mut context, gid, _peer) = writable_fixture(
12542 &pool,
12543 &[FIRST_BLOCK, SECOND_BLOCK],
12544 &[(0, SocketResult::WouldBlock)],
12545 );
12546 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
12547
12548 let parked = H2StreamId::Other {
12549 id: REGISTERED_STREAM_ID,
12550 gid,
12551 };
12552 connection.core.stream_table.set_expect_write(Some(parked));
12553 assert_prepare_gate_is_shut(&context, gid);
12554
12555 let result = connection.writable(&mut context, EndpointClient(&mut router));
12556
12557 assert_eq!(
12558 connection.core.stream_table.expect_write(),
12559 Some(parked),
12560 "a stalled resume must leave expect_write parked: clearing it \
12561 would hand a socket-blocked stream back to the scheduler"
12562 );
12563 assert_eq!(
12564 connection.socket.vectored_calls, 1,
12565 "a stalled resume must return before the scheduler pass begins, so \
12566 the registered stream must not be flushed a second time"
12567 );
12568 assert!(
12569 !context.streams[gid].back.out.is_empty(),
12570 "the refused blocks must stay queued for the next pass"
12571 );
12572 assert_eq!(
12573 context.streams[gid].metrics.bout, 0,
12574 "a stalled resume delivered nothing"
12575 );
12576 assert!(
12577 matches!(result, MuxResult::Continue),
12578 "a stalled resume continues the session, got {result:?}"
12579 );
12580 }
12581
12582 /// A socket that takes at most `budget` bytes, then answers
12583 /// `WouldBlock`, and keeps every byte it took in `wire`, in order: what
12584 /// the peer would read. Reads are served from `inbound`.
12585 struct PacedSocket {
12586 stream: mio::net::TcpStream,
12587 budget: usize,
12588 wire: Vec<u8>,
12589 inbound: std::collections::VecDeque<u8>,
12590 }
12591
12592 impl PacedSocket {
12593 fn take(&mut self, buf: &[u8]) -> usize {
12594 let taken = self.budget.min(buf.len());
12595 self.wire.extend_from_slice(&buf[..taken]);
12596 self.budget -= taken;
12597 taken
12598 }
12599 }
12600
12601 impl SocketHandler for PacedSocket {
12602 fn socket_read(&mut self, buf: &mut [u8]) -> (usize, SocketResult) {
12603 let size = buf.len().min(self.inbound.len());
12604 for (slot, byte) in buf.iter_mut().zip(self.inbound.drain(..size)) {
12605 *slot = byte;
12606 }
12607 if size < buf.len() {
12608 (size, SocketResult::WouldBlock)
12609 } else {
12610 (size, SocketResult::Continue)
12611 }
12612 }
12613
12614 fn socket_write(&mut self, buf: &[u8]) -> (usize, SocketResult) {
12615 let taken = self.take(buf);
12616 if taken < buf.len() {
12617 (taken, SocketResult::WouldBlock)
12618 } else {
12619 (taken, SocketResult::Continue)
12620 }
12621 }
12622
12623 fn socket_write_vectored(&mut self, bufs: &[IoSlice]) -> (usize, SocketResult) {
12624 let mut total = 0;
12625 for buf in bufs {
12626 let taken = self.take(buf);
12627 total += taken;
12628 if taken < buf.len() {
12629 return (total, SocketResult::WouldBlock);
12630 }
12631 }
12632 (total, SocketResult::Continue)
12633 }
12634
12635 fn socket_ref(&self) -> &mio::net::TcpStream {
12636 &self.stream
12637 }
12638
12639 fn socket_mut(&mut self) -> &mut mio::net::TcpStream {
12640 &mut self.stream
12641 }
12642
12643 fn peer_addr(&self) -> Option<std::net::SocketAddr> {
12644 None
12645 }
12646
12647 fn protocol(&self) -> crate::socket::TransportProtocol {
12648 crate::socket::TransportProtocol::Tcp
12649 }
12650
12651 fn read_error(&self) {}
12652
12653 fn write_error(&self) {}
12654 }
12655
12656 /// One frame as the peer reads it: `(type, flags, stream id, payload)`.
12657 type PeerFrame = (u8, u8, u32, Vec<u8>);
12658
12659 /// Split `wire` into frames the way the peer's parser does, failing on a
12660 /// frame the bytes cannot hold.
12661 fn peer_frames(mut wire: &[u8]) -> Result<Vec<PeerFrame>, String> {
12662 let mut frames = Vec::new();
12663 while !wire.is_empty() {
12664 if wire.len() < 9 {
12665 return Err(format!("{} trailing bytes: {wire:?}", wire.len()));
12666 }
12667 let len = u32::from_be_bytes([0, wire[0], wire[1], wire[2]]) as usize;
12668 let stream_id = u32::from_be_bytes([wire[5], wire[6], wire[7], wire[8]]) & 0x7fff_ffff;
12669 if wire.len() < 9 + len {
12670 return Err(format!(
12671 "frame type {:#x} announces {len} bytes, {} remain",
12672 wire[3],
12673 wire.len() - 9
12674 ));
12675 }
12676 frames.push((wire[3], wire[4], stream_id, wire[9..9 + len].to_vec()));
12677 wire = &wire[9 + len..];
12678 }
12679 Ok(frames)
12680 }
12681
12682 /// The payload of the DATA frame `a_ping_during_a_half_written_frame_*`
12683 /// queues on stream 1, after its header below.
12684 const HALF_WRITTEN_PAYLOAD: &[u8] = &[b'd'; 32];
12685 const HALF_WRITTEN_HEADER: &[u8] = &[0, 0, 32, 0, 0, 0, 0, 0, 1];
12686
12687 /// Park a 41-byte DATA frame on stream 1 after the socket took 20 bytes
12688 /// of it, feed `inbound` (a PING or SETTINGS), and read it. `between`
12689 /// then runs before the socket is unthrottled, and the connection is
12690 /// driven the way `Mux` drives it, `writable()` and `readable()` when
12691 /// the filtered readiness allows them. Returns the connection, whose
12692 /// socket holds every byte the peer would read.
12693 fn ack_after_half_written_frame(
12694 inbound: &[u8],
12695 between: impl FnOnce(
12696 &mut H2Shell<PacedSocket>,
12697 &mut Context<TestListener>,
12698 &mut Router,
12699 GlobalStreamId,
12700 ),
12701 ) -> H2Shell<PacedSocket> {
12702 let (_pool, mut connection, mut context, mut router, gid) = park_half_written_frame();
12703
12704 connection.socket.inbound.extend(inbound);
12705 for _ in 0..8 {
12706 connection.core.readiness.event.insert(Ready::READABLE);
12707 if !connection.core.readiness.filter_interest().is_readable() {
12708 break;
12709 }
12710 connection.readable(&mut context, EndpointClient(&mut router));
12711 }
12712 assert!(
12713 connection.socket.inbound.is_empty() && !connection.core.output.is_empty(),
12714 "premise: the frame was read and its ACK queued"
12715 );
12716
12717 between(&mut connection, &mut context, &mut router, gid);
12718 drive_unthrottled(&mut connection, &mut context, &mut router);
12719 connection
12720 }
12721
12722 /// Drive `connection` the way `Mux` does, `writable()` and `readable()`
12723 /// when the filtered readiness allows them, with the socket unthrottled.
12724 fn drive_unthrottled(
12725 connection: &mut H2Shell<PacedSocket>,
12726 context: &mut Context<TestListener>,
12727 router: &mut Router,
12728 ) {
12729 connection.socket.budget = usize::MAX;
12730 for _ in 0..16 {
12731 connection.core.readiness.event.insert(Ready::WRITABLE);
12732 if connection.core.readiness.filter_interest().is_writable() {
12733 connection.writable(context, EndpointClient(router));
12734 }
12735 connection.core.readiness.event.insert(Ready::READABLE);
12736 if connection.core.readiness.filter_interest().is_readable() {
12737 connection.readable(context, EndpointClient(router));
12738 }
12739 }
12740 }
12741
12742 /// Park a 41-byte DATA frame on stream 1 after the socket took 20 bytes
12743 /// of it, and return the buffer pool, which must outlive the rest, the
12744 /// connection, its context and router, and the stream's global id.
12745 fn park_half_written_frame() -> (
12746 Rc<RefCell<Pool>>,
12747 H2Shell<PacedSocket>,
12748 Context<TestListener>,
12749 Router,
12750 GlobalStreamId,
12751 ) {
12752 park_cut_frames(&[HALF_WRITTEN_HEADER, HALF_WRITTEN_PAYLOAD], 20)
12753 }
12754
12755 /// [`park_half_written_frame`] for any whole frames of stream 1, queued
12756 /// as `blocks`, of which the socket takes `budget` bytes.
12757 fn park_cut_frames(
12758 blocks: &[&'static [u8]],
12759 budget: usize,
12760 ) -> (
12761 Rc<RefCell<Pool>>,
12762 H2Shell<PacedSocket>,
12763 Context<TestListener>,
12764 Router,
12765 GlobalStreamId,
12766 ) {
12767 let pool = make_pool_for_invariant_16();
12768 let (socket, _peer) = connected_socket();
12769 let mut connection = H2Shell::new(
12770 Ulid::generate(),
12771 PacedSocket {
12772 stream: socket,
12773 budget,
12774 wire: Vec::new(),
12775 inbound: std::collections::VecDeque::new(),
12776 },
12777 Position::Server,
12778 &mut PoolBufferSource::new(Rc::downgrade(&pool)),
12779 H2FloodConfig::default(),
12780 H2ConnectionConfig::default(),
12781 Duration::from_secs(30),
12782 None,
12783 Duration::from_secs(30),
12784 Some((H2StreamId::Zero, 9)),
12785 Ready::READABLE | Ready::WRITABLE | Ready::HUP | Ready::ERROR,
12786 )
12787 .expect("a pool with free buffers must yield an H2 connection");
12788 connection.core.state = H2State::Header;
12789 let mut context = test_context(&pool);
12790 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
12791 let gid = context
12792 .create_stream(Ulid::generate(), 1 << 16)
12793 .expect("test context must create a stream");
12794 connection
12795 .core
12796 .stream_table
12797 .register(REGISTERED_STREAM_ID, gid, connection.core.now);
12798 for &block in blocks {
12799 context.streams[gid]
12800 .back
12801 .out
12802 .push_back(kawa::OutBlock::Store(kawa::Store::Static(block)));
12803 }
12804 assert_prepare_gate_is_shut(&context, gid);
12805
12806 connection.core.readiness.event.insert(Ready::WRITABLE);
12807 connection.writable(&mut context, EndpointClient(&mut router));
12808 assert_eq!(
12809 (
12810 connection.socket.wire.len(),
12811 connection.core.stream_table.expect_write()
12812 ),
12813 (
12814 budget,
12815 Some(H2StreamId::Other {
12816 id: REGISTERED_STREAM_ID,
12817 gid
12818 })
12819 ),
12820 "premise: the frames are cut and their stream parked"
12821 );
12822 (pool, connection, context, router, gid)
12823 }
12824
12825 const HALF_WRITTEN_PING: &[u8] = b"\x00\x00\x08\x06\x00\x00\x00\x00\x00pingpong";
12826 const SECOND_PING: &[u8] = b"\x00\x00\x08\x06\x00\x00\x00\x00\x00pingpon2";
12827
12828 /// `(type, flags, stream id)` of every frame the peer reads, or the
12829 /// parse failure.
12830 fn peer_frame_kinds(wire: &[u8]) -> Result<Vec<(u8, u8, u32)>, String> {
12831 peer_frames(wire).map(|frames| {
12832 frames
12833 .into_iter()
12834 .map(|(kind, flags, stream_id, _)| (kind, flags, stream_id))
12835 .collect()
12836 })
12837 }
12838
12839 /// A PING that arrives while a stream frame is half-written must be
12840 /// acknowledged AFTER that frame, never inside it (#1600).
12841 ///
12842 /// The socket takes 20 bytes of a 41-byte DATA frame, so the stream is
12843 /// parked in `expect_write` and the 21-byte rest of its frame is adopted
12844 /// by the ordered output queue (#1604). The peer then sends a PING. Its
12845 /// ACK is queued behind that rest, so the peer reads the whole DATA
12846 /// frame, then the ACK, and never 17 ACK bytes in the middle of the
12847 /// frame.
12848 ///
12849 /// TO SEE THIS RED: in `ConnectionH2::handle_write`, replace
12850 /// `self.output.adopt_tail(kawa, tail);` by `let _ = (kawa, tail);`, so
12851 /// the rest of the cut frame stays with its stream. Every test on this
12852 /// fixture then fails: this one and the other #1601 tests on
12853 /// `frame_tail`'s `a frame runs past the gathered bytes` debug assertion
12854 /// (the stream's `kawa.out` no longer starts on a frame boundary), the
12855 /// three #1604 tests below on their wire assertion. Verified 2026-09-27.
12856 /// Before #1604 the equivalent mutation was in `queue_zero_output` and
12857 /// failed with `left: Err("frame type 0x64 announces 6579300 bytes, 8
12858 /// remain")` (verified 2026-09-26).
12859 #[test]
12860 fn a_ping_during_a_half_written_frame_is_acknowledged_after_it() {
12861 let connection = ack_after_half_written_frame(HALF_WRITTEN_PING, |_, _, _, _| {});
12862
12863 assert_eq!(
12864 peer_frames(&connection.socket.wire),
12865 Ok(vec![
12866 (0, 0, REGISTERED_STREAM_ID, HALF_WRITTEN_PAYLOAD.to_vec()),
12867 (6, parser::FLAG_ACK, 0, b"pingpong".to_vec()),
12868 ]),
12869 "the peer must read the whole DATA frame, then the PING ACK"
12870 );
12871 assert!(
12872 connection.core.readiness.interest.is_readable(),
12873 "once the ACK is out the connection must read again"
12874 );
12875 }
12876
12877 /// The SETTINGS ACK takes the same path as the PING ACK above (#1600).
12878 #[test]
12879 fn a_settings_during_a_half_written_frame_is_acknowledged_after_it() {
12880 let connection =
12881 ack_after_half_written_frame(b"\x00\x00\x00\x04\x00\x00\x00\x00\x00", |_, _, _, _| {});
12882
12883 assert_eq!(
12884 peer_frames(&connection.socket.wire),
12885 Ok(vec![
12886 (0, 0, REGISTERED_STREAM_ID, HALF_WRITTEN_PAYLOAD.to_vec()),
12887 (4, parser::FLAG_ACK, 0, Vec::new()),
12888 ]),
12889 "the peer must read the whole DATA frame, then the SETTINGS ACK"
12890 );
12891 assert!(
12892 connection.core.readiness.interest.is_readable(),
12893 "once the ACK is out the connection must read again"
12894 );
12895 }
12896
12897 /// An ACK queued behind a half-written frame whose stream is removed
12898 /// before the frame resumes still leaves, and the connection keeps
12899 /// reading: the removal takes nothing that was queued. What the peer
12900 /// reads is asserted by
12901 /// `a_removed_half_written_stream_still_completes_its_frame` (#1604).
12902 ///
12903 /// TO SEE THIS RED: in `ConnectionH2::flush_pending_control_frames`,
12904 /// answer `H2ControlFlushTarget::Proceed` unconditionally at its end. The
12905 /// test then fails with `an ACK behind a removed stream's frame must not
12906 /// be stranded`.
12907 #[test]
12908 fn a_deferred_ack_does_not_strand_reads_when_its_parked_stream_is_removed() {
12909 let connection =
12910 ack_after_half_written_frame(HALF_WRITTEN_PING, |connection, _, _, gid| {
12911 connection
12912 .core
12913 .remove_dead_stream(REGISTERED_STREAM_ID, gid);
12914 });
12915
12916 assert!(
12917 connection.core.output.is_empty(),
12918 "an ACK behind a removed stream's frame must not be stranded"
12919 );
12920 assert!(
12921 connection.core.readiness.interest.is_readable(),
12922 "once the output is flushed the connection must read again"
12923 );
12924 }
12925
12926 /// Two PINGs read while one frame stays parked are both acknowledged,
12927 /// in order, after that frame.
12928 #[test]
12929 fn two_pings_during_one_park_are_acknowledged_after_the_frame() {
12930 let connection = ack_after_half_written_frame(HALF_WRITTEN_PING, |connection, _, _, _| {
12931 connection.socket.inbound.extend(SECOND_PING);
12932 });
12933
12934 assert_eq!(
12935 peer_frames(&connection.socket.wire),
12936 Ok(vec![
12937 (0, 0, REGISTERED_STREAM_ID, HALF_WRITTEN_PAYLOAD.to_vec()),
12938 (6, parser::FLAG_ACK, 0, b"pingpong".to_vec()),
12939 (6, parser::FLAG_ACK, 0, b"pingpon2".to_vec()),
12940 ]),
12941 "both PING ACKs must follow the whole DATA frame, in order"
12942 );
12943 }
12944
12945 /// A SETTINGS then a PING during one park: both ACKs, in order.
12946 #[test]
12947 fn settings_then_ping_during_one_park_are_acknowledged_after_the_frame() {
12948 let connection = ack_after_half_written_frame(
12949 b"\x00\x00\x00\x04\x00\x00\x00\x00\x00",
12950 |connection, _, _, _| {
12951 connection.socket.inbound.extend(SECOND_PING);
12952 },
12953 );
12954
12955 assert_eq!(
12956 peer_frames(&connection.socket.wire),
12957 Ok(vec![
12958 (0, 0, REGISTERED_STREAM_ID, HALF_WRITTEN_PAYLOAD.to_vec()),
12959 (4, parser::FLAG_ACK, 0, Vec::new()),
12960 (6, parser::FLAG_ACK, 0, b"pingpon2".to_vec()),
12961 ]),
12962 "the SETTINGS ACK then the PING ACK must follow the whole DATA frame"
12963 );
12964 }
12965
12966 /// The shutdown drive (`Mux::shutting_down`) calls `flush_output_buffer`,
12967 /// then forces READABLE and calls `readable()`. That read must not eat
12968 /// the queued ACK. Since #1604 it cannot by construction: the read lands
12969 /// in `zero`, which is input only, and the ACK waits in the output queue
12970 /// behind the rest of the half-written frame.
12971 ///
12972 /// TO SEE THIS RED: the `adopt_tail` mutation described on
12973 /// `a_ping_during_a_half_written_frame_is_acknowledged_after_it` fails it
12974 /// on `a frame runs past the gathered bytes`. Verified 2026-09-27. Before
12975 /// #1604, deleting the `zero_holds_output()` block of `poll_read_target`
12976 /// failed it on `no output may be pending in zero when a frame header is
12977 /// read` (verified 2026-09-27).
12978 #[test]
12979 fn a_forced_shutdown_read_does_not_eat_a_deferred_ack() {
12980 let connection =
12981 ack_after_half_written_frame(HALF_WRITTEN_PING, |connection, context, router, _| {
12982 connection.socket.inbound.extend(SECOND_PING);
12983 connection.flush_output_buffer();
12984 connection.core.readiness.interest.insert(Ready::READABLE);
12985 connection.core.readiness.event.insert(Ready::READABLE);
12986 connection.readable(context, EndpointClient(router));
12987 });
12988
12989 assert_eq!(
12990 peer_frames(&connection.socket.wire),
12991 Ok(vec![
12992 (0, 0, REGISTERED_STREAM_ID, HALF_WRITTEN_PAYLOAD.to_vec()),
12993 (6, parser::FLAG_ACK, 0, b"pingpong".to_vec()),
12994 (6, parser::FLAG_ACK, 0, b"pingpon2".to_vec()),
12995 ]),
12996 "the forced shutdown read must not eat the deferred ACK"
12997 );
12998 }
12999
13000 /// A final GOAWAY while a frame is parked and an ACK queued is sent after
13001 /// both: `goaway()` appends to the ordered output queue, behind the rest
13002 /// of the frame and the ACK.
13003 ///
13004 /// TO SEE THIS RED: in `ConnectionH2::goaway`, call `self.output.clear()`
13005 /// before queueing the GOAWAY. The test then fails with `the whole DATA
13006 /// frame, the PING ACK, then the GOAWAY`. Verified 2026-09-27.
13007 #[test]
13008 fn a_goaway_during_a_half_written_frame_follows_it_and_the_ack() {
13009 let connection =
13010 ack_after_half_written_frame(HALF_WRITTEN_PING, |connection, context, _, gid| {
13011 // A stream being answered is linked; an idle one would be
13012 // pruned by the closing connection before its frame resumes.
13013 context.streams[gid].state = StreamState::Linked(mio::Token(1));
13014 let _ = connection.core.goaway(H2Error::NoError);
13015 });
13016
13017 assert_eq!(
13018 peer_frame_kinds(&connection.socket.wire),
13019 Ok(vec![
13020 (0, 0, REGISTERED_STREAM_ID),
13021 (6, parser::FLAG_ACK, 0),
13022 (7, 0, 0)
13023 ]),
13024 "the whole DATA frame, the PING ACK, then the GOAWAY"
13025 );
13026 }
13027
13028 /// The initial graceful GOAWAY (`graceful_goaway` -> `send_initial_goaway`,
13029 /// the call `Mux::shutting_down` makes) behaves like the final one.
13030 ///
13031 /// TO SEE THIS RED: in `ConnectionH2::send_initial_goaway`, call
13032 /// `self.output.clear()` before queueing the GOAWAY. The test then fails
13033 /// with `the whole DATA frame, the PING ACK, then the initial GOAWAY`.
13034 /// Verified 2026-09-27.
13035 #[test]
13036 fn an_initial_goaway_during_a_half_written_frame_follows_it_and_the_ack() {
13037 let connection = ack_after_half_written_frame(HALF_WRITTEN_PING, |connection, _, _, _| {
13038 let now = connection.core.now;
13039 let _ = connection.core.graceful_goaway(now);
13040 });
13041
13042 assert_eq!(
13043 peer_frame_kinds(&connection.socket.wire),
13044 Ok(vec![
13045 (0, 0, REGISTERED_STREAM_ID),
13046 (6, parser::FLAG_ACK, 0),
13047 (7, 0, 0)
13048 ]),
13049 "the whole DATA frame, the PING ACK, then the initial GOAWAY"
13050 );
13051 }
13052
13053 /// A peer RST_STREAM for stream 1, whose DATA frame the fixture parks.
13054 const RST_STREAM_1: &[u8] = b"\x00\x00\x04\x03\x00\x00\x00\x00\x01\x00\x00\x00\x08";
13055
13056 /// A stream parked half-way through a frame and then removed must not
13057 /// leave that frame truncated on the wire (#1604): the bytes written
13058 /// after the removal would land inside what the peer still parses as
13059 /// that frame. Here the removal is `remove_dead_stream` itself, the
13060 /// chokepoint of every removal path, and the next frame is a PING ACK
13061 /// read after the removal.
13062 ///
13063 /// TO SEE THIS RED: on the pre-image, `a0785505` (stream frames written
13064 /// from `kawa.out` and serialised only through `expect_write`), this test
13065 /// and its two siblings below fail on their wire assertion with, in
13066 /// order, `left: Err("frame type 0x0 announces 65536 bytes, 4 remain")`,
13067 /// `... 0 remain")` and `left: Err("frame type 0x0 announces 32 bytes,
13068 /// 28 remain")`: the peer reads the next frame's bytes as the rest of the
13069 /// truncated one. On this tree, the `adopt_tail` mutation described on
13070 /// `a_ping_during_a_half_written_frame_is_acknowledged_after_it` fails
13071 /// all three with the same messages. Verified 2026-09-27.
13072 #[test]
13073 fn a_removed_half_written_stream_still_completes_its_frame() {
13074 let connection =
13075 ack_after_half_written_frame(HALF_WRITTEN_PING, |connection, _, _, gid| {
13076 connection
13077 .core
13078 .remove_dead_stream(REGISTERED_STREAM_ID, gid);
13079 connection.socket.inbound.extend(SECOND_PING);
13080 });
13081
13082 assert_eq!(
13083 peer_frames(&connection.socket.wire),
13084 Ok(vec![
13085 (0, 0, REGISTERED_STREAM_ID, HALF_WRITTEN_PAYLOAD.to_vec()),
13086 (6, parser::FLAG_ACK, 0, b"pingpong".to_vec()),
13087 (6, parser::FLAG_ACK, 0, b"pingpon2".to_vec()),
13088 ]),
13089 "the removed stream's frame must be completed before the ACKs"
13090 );
13091 assert!(
13092 connection.core.readiness.interest.is_readable(),
13093 "once the output is flushed the connection must read again"
13094 );
13095 }
13096
13097 /// The same removal followed by another stream's frame (#1604): the
13098 /// next stream's DATA frame must follow the whole parked frame.
13099 #[test]
13100 fn a_removed_half_written_stream_does_not_corrupt_the_next_stream_frame() {
13101 const NEXT_STREAM_ID: StreamId = 3;
13102 const NEXT_HEADER: &[u8] = &[0, 0, 4, 0, 1, 0, 0, 0, 3];
13103 const NEXT_PAYLOAD: &[u8] = b"next";
13104 let connection =
13105 ack_after_half_written_frame(HALF_WRITTEN_PING, |connection, context, _, gid| {
13106 connection
13107 .core
13108 .remove_dead_stream(REGISTERED_STREAM_ID, gid);
13109 let next = context
13110 .create_stream(Ulid::generate(), 1 << 16)
13111 .expect("test context must create a second stream");
13112 connection
13113 .core
13114 .stream_table
13115 .register(NEXT_STREAM_ID, next, connection.core.now);
13116 for block in [NEXT_HEADER, NEXT_PAYLOAD] {
13117 context.streams[next]
13118 .back
13119 .out
13120 .push_back(kawa::OutBlock::Store(kawa::Store::Static(block)));
13121 }
13122 });
13123
13124 assert_eq!(
13125 peer_frames(&connection.socket.wire),
13126 Ok(vec![
13127 (0, 0, REGISTERED_STREAM_ID, HALF_WRITTEN_PAYLOAD.to_vec()),
13128 (6, parser::FLAG_ACK, 0, b"pingpong".to_vec()),
13129 (0, 1, NEXT_STREAM_ID, NEXT_PAYLOAD.to_vec()),
13130 ]),
13131 "the next stream's frame must follow the whole parked frame"
13132 );
13133 }
13134
13135 /// HEADERS without END_HEADERS, then its CONTINUATION, on stream 1.
13136 const CUT_BLOCK_HEADERS: &[u8] = b"\x00\x00\x10\x01\x00\x00\x00\x00\x01hpack-fragment-1";
13137 const CUT_BLOCK_CONTINUATION: &[u8] = b"\x00\x00\x10\x09\x04\x00\x00\x00\x01hpack-fragment-2";
13138
13139 /// A header block cut by a partial write keeps everything else out until
13140 /// END_HEADERS (RFC 9113 §6.10): the PING ACK read meanwhile follows the
13141 /// CONTINUATION, whether the write stopped inside the HEADERS frame or
13142 /// exactly on the boundary between the two frames, where no frame is
13143 /// half-written but the block still is (#1604).
13144 ///
13145 /// TO SEE THIS RED: in `h2_transmit::frame_tail`, append `&& false` to
13146 /// the `in_block` expression, so the rest stops at the cut frame's end.
13147 /// The test then fails with `the ACK must follow END_HEADERS`, the ACK
13148 /// sitting between the HEADERS and the CONTINUATION. Verified
13149 /// 2026-09-27.
13150 #[test]
13151 fn a_cut_header_block_is_completed_before_the_ack() {
13152 for budget in [20, CUT_BLOCK_HEADERS.len()] {
13153 let (_pool, mut connection, mut context, mut router, _gid) =
13154 park_cut_frames(&[CUT_BLOCK_HEADERS, CUT_BLOCK_CONTINUATION], budget);
13155 connection.socket.inbound.extend(HALF_WRITTEN_PING);
13156 for _ in 0..4 {
13157 connection.core.readiness.event.insert(Ready::READABLE);
13158 connection.readable(&mut context, EndpointClient(&mut router));
13159 }
13160 assert!(
13161 connection.socket.inbound.is_empty(),
13162 "premise: the PING was read while the block is cut"
13163 );
13164 drive_unthrottled(&mut connection, &mut context, &mut router);
13165
13166 assert_eq!(
13167 peer_frame_kinds(&connection.socket.wire),
13168 Ok(vec![
13169 (1, 0, REGISTERED_STREAM_ID),
13170 (9, parser::FLAG_END_HEADERS, REGISTERED_STREAM_ID),
13171 (6, parser::FLAG_ACK, 0),
13172 ]),
13173 "the ACK must follow END_HEADERS (cut at {budget})"
13174 );
13175 }
13176 }
13177
13178 /// The peer resets the parked stream (#1604). Its RST_STREAM is read
13179 /// while the frame is half-written, and retires the stream; the rest of
13180 /// that frame must still reach the wire before the PING ACK behind it.
13181 #[test]
13182 fn a_peer_reset_of_a_half_written_stream_still_completes_its_frame() {
13183 let (_pool, mut connection, mut context, mut router, _gid) = park_half_written_frame();
13184 connection.socket.inbound.extend(RST_STREAM_1);
13185 connection.core.readiness.event.insert(Ready::READABLE);
13186 connection.readable(&mut context, EndpointClient(&mut router));
13187 connection.core.readiness.event.insert(Ready::READABLE);
13188 connection.readable(&mut context, EndpointClient(&mut router));
13189 assert!(
13190 connection.socket.inbound.is_empty()
13191 && !connection
13192 .core
13193 .stream_table
13194 .streams()
13195 .contains_key(®ISTERED_STREAM_ID),
13196 "premise: the RST was read and retired the parked stream"
13197 );
13198
13199 connection.socket.inbound.extend(HALF_WRITTEN_PING);
13200 drive_unthrottled(&mut connection, &mut context, &mut router);
13201
13202 assert_eq!(
13203 peer_frames(&connection.socket.wire),
13204 Ok(vec![
13205 (0, 0, REGISTERED_STREAM_ID, HALF_WRITTEN_PAYLOAD.to_vec()),
13206 (6, parser::FLAG_ACK, 0, b"pingpong".to_vec()),
13207 ]),
13208 "the reset stream's frame must be completed before the ACK"
13209 );
13210 }
13211
13212 /// Park stream 1's encoded 404 unsent behind a socket that takes nothing,
13213 /// let `drop_block` discard it, then drive the connection unthrottled and
13214 /// decode every HEADERS the peer reads with ONE decoder, as the peer does.
13215 /// Answers `(stream id, decoded)` per HEADERS and the `:status` values read.
13216 ///
13217 /// Both requests get the same 404, so stream 1's block inserts the
13218 /// answer's fields into the connection's encoder table and stream 3's
13219 /// identical block names them by index: a peer that missed stream 1's
13220 /// block cannot read stream 3's without a table reset.
13221 fn peer_reads_after_a_dropped_parked_block(
13222 drop_block: impl FnOnce(&mut H2Shell<PacedSocket>, &mut Context<TestListener>, &mut Router),
13223 ) -> (Vec<(u32, bool)>, Vec<Vec<u8>>) {
13224 let (_pool, mut connection, mut context, mut router, _peer) = two_requests_read(0);
13225 let answers = context.listener.borrow().get_answers().clone();
13226 let open: Vec<GlobalStreamId> = connection
13227 .core
13228 .stream_table
13229 .streams()
13230 .values()
13231 .copied()
13232 .collect();
13233 for gid in open {
13234 crate::protocol::mux::answers::set_default_answer(
13235 &mut context.streams[gid],
13236 &mut connection.core.readiness,
13237 404,
13238 &answers.borrow(),
13239 );
13240 }
13241 connection.core.readiness.event.insert(Ready::WRITABLE);
13242 connection.writable(&mut context, EndpointClient(&mut router));
13243 assert!(
13244 connection.socket.wire.is_empty()
13245 && connection.core.stream_table.expect_write().is_some(),
13246 "premise: stream 1's block was encoded and parked unsent"
13247 );
13248 drop_block(&mut connection, &mut context, &mut router);
13249 connection.socket.budget = usize::MAX;
13250 drive_both_ways(&mut connection, &mut context, &mut router);
13251 peer_header_blocks(&connection.socket.wire)
13252 }
13253
13254 /// An H2 server connection over a [`PacedSocket`] taking `budget` bytes,
13255 /// with GET requests on streams 1 and 3 read and not answered.
13256 fn two_requests_read(
13257 budget: usize,
13258 ) -> (
13259 Rc<RefCell<Pool>>,
13260 H2Shell<PacedSocket>,
13261 Context<TestListener>,
13262 Router,
13263 std::net::TcpStream,
13264 ) {
13265 let pool = make_pool_for_invariant_16();
13266 let (socket, peer) = connected_socket();
13267 let mut connection = H2Shell::new(
13268 Ulid::generate(),
13269 PacedSocket {
13270 stream: socket,
13271 budget,
13272 wire: Vec::new(),
13273 inbound: std::collections::VecDeque::new(),
13274 },
13275 Position::Server,
13276 &mut PoolBufferSource::new(Rc::downgrade(&pool)),
13277 H2FloodConfig::default(),
13278 H2ConnectionConfig::default(),
13279 Duration::from_secs(30),
13280 None,
13281 Duration::from_secs(30),
13282 Some((H2StreamId::Zero, 9)),
13283 Ready::READABLE | Ready::WRITABLE | Ready::HUP | Ready::ERROR,
13284 )
13285 .expect("pool");
13286 connection.core.state = H2State::Header;
13287 let mut context = test_context(&pool);
13288 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
13289 let mut peer_encoder = crate::protocol::mux::hpack::Encoder::new();
13290 for stream_id in [1u32, 3] {
13291 let block = peer_encoder.encode([
13292 (&b":method"[..], &b"GET"[..]),
13293 (&b":scheme"[..], &b"https"[..]),
13294 (&b":authority"[..], &b"example.com"[..]),
13295 (&b":path"[..], &b"/repro"[..]),
13296 ]);
13297 let mut frame = (block.len() as u32).to_be_bytes()[1..].to_vec();
13298 frame.push(1);
13299 frame.push(parser::FLAG_END_STREAM | parser::FLAG_END_HEADERS);
13300 frame.extend_from_slice(&stream_id.to_be_bytes());
13301 frame.extend_from_slice(&block);
13302 connection.socket.inbound.extend(frame);
13303 }
13304 for _ in 0..8 {
13305 connection.core.readiness.event.insert(Ready::READABLE);
13306 connection.readable(&mut context, EndpointClient(&mut router));
13307 }
13308 assert_eq!(
13309 connection.core.stream_table.len(),
13310 2,
13311 "premise: both requests were read"
13312 );
13313 (pool, connection, context, router, peer)
13314 }
13315
13316 /// Drive the connection the way `Mux` does, `writable()` and
13317 /// `readable()` when the filtered readiness allows them.
13318 fn drive_both_ways(
13319 connection: &mut H2Shell<PacedSocket>,
13320 context: &mut Context<TestListener>,
13321 router: &mut Router,
13322 ) {
13323 for _ in 0..16 {
13324 connection.core.readiness.event.insert(Ready::WRITABLE);
13325 if connection.core.readiness.filter_interest().is_writable() {
13326 connection.writable(context, EndpointClient(router));
13327 }
13328 connection.core.readiness.event.insert(Ready::READABLE);
13329 if connection.core.readiness.filter_interest().is_readable() {
13330 connection.readable(context, EndpointClient(router));
13331 }
13332 }
13333 }
13334
13335 /// Decode every HEADERS on `wire` with ONE decoder, as the peer does.
13336 /// Answers `(stream id, decoded)` per HEADERS and the `:status` values read.
13337 fn peer_header_blocks(wire: &[u8]) -> (Vec<(u32, bool)>, Vec<Vec<u8>>) {
13338 let mut decoder = crate::protocol::mux::hpack::Decoder::new();
13339 let mut seen = Vec::new();
13340 let mut status = Vec::new();
13341 for (kind, _, stream_id, payload) in peer_frames(wire).unwrap() {
13342 if kind == 1 {
13343 let decoded = decoder.decode_with_cb(&payload, |name, value| {
13344 if &*name == b":status" {
13345 status.push(value.into_owned());
13346 }
13347 });
13348 seen.push((stream_id, decoded.is_ok()));
13349 }
13350 }
13351 (seen, status)
13352 }
13353
13354 /// A header block the encoder produced but the wire never started is
13355 /// dropped with its stream, and the peer's HPACK table must not fall
13356 /// behind for it (sozu-proxy/sozu#1627, path 1): the peer's RST_STREAM(1)
13357 /// removes the parked stream 1, and stream 3's block must still decode.
13358 ///
13359 /// TO SEE THIS RED: in `ConnectionH2::poll_write_target`'s
13360 /// `H2WritePhase::Start` arm, delete the `self.reset_encoder_table()` call.
13361 /// The peer then fails to decode stream 3's block: `left: [(3, false)],
13362 /// right: [(3, true)]`. Verified 2026-09-27 (red on `58550dd4`).
13363 #[test]
13364 fn a_header_block_dropped_with_its_stream_keeps_the_peer_table_in_sync() {
13365 let (seen, status) =
13366 peer_reads_after_a_dropped_parked_block(|connection, context, router| {
13367 connection.socket.inbound.extend(RST_STREAM_1);
13368 for _ in 0..4 {
13369 connection.core.readiness.event.insert(Ready::READABLE);
13370 connection.readable(context, EndpointClient(router));
13371 }
13372 assert!(
13373 !connection.core.stream_table.streams().contains_key(&1),
13374 "premise: the RST removed stream 1"
13375 );
13376 });
13377 assert_eq!(
13378 seen,
13379 vec![(3, true)],
13380 "the peer must decode stream 3's block with its own table"
13381 );
13382 assert_eq!(status, vec![b"404".to_vec()], "and read what was encoded");
13383 }
13384
13385 /// The park outlives a second write pass that is stalled too: that pass
13386 /// resumes the parked stream, writes nothing, and parks it again, so it
13387 /// must record again that the park holds an encoded block. The peer's
13388 /// RST_STREAM(1) then removes it, and stream 3's block must still decode
13389 /// (sozu-proxy/sozu#1627).
13390 ///
13391 /// TO SEE THIS RED: in `ConnectionH2::poll_write_target`'s
13392 /// `H2WritePhase::Resume` arm, delete the `self.parked_header_block = ..`
13393 /// assignment before the stalled `return`. The peer then fails on stream
13394 /// 3's block: `left: [(3, false)], right: [(3, true)]`. Verified
13395 /// 2026-09-27.
13396 #[test]
13397 fn a_header_block_parked_across_two_stalled_passes_keeps_the_peer_table_in_sync() {
13398 let (seen, status) =
13399 peer_reads_after_a_dropped_parked_block(|connection, context, router| {
13400 connection.core.readiness.event.insert(Ready::WRITABLE);
13401 connection.writable(context, EndpointClient(router));
13402 assert!(
13403 connection.socket.wire.is_empty()
13404 && connection.core.stream_table.expect_write().is_some(),
13405 "premise: the second pass resumed nothing and kept the park"
13406 );
13407 connection.socket.inbound.extend(RST_STREAM_1);
13408 for _ in 0..4 {
13409 connection.core.readiness.event.insert(Ready::READABLE);
13410 connection.readable(context, EndpointClient(router));
13411 }
13412 assert!(
13413 !connection.core.stream_table.streams().contains_key(&1),
13414 "premise: the RST removed stream 1"
13415 );
13416 });
13417 assert_eq!(
13418 seen,
13419 vec![(3, true)],
13420 "the peer must decode stream 3's block with its own table"
13421 );
13422 assert_eq!(status, vec![b"404".to_vec()], "and read what was encoded");
13423 }
13424
13425 /// An H2 backend connection (`Position::Client`, `Connected`, handshake
13426 /// done) over a [`PacedSocket`] taking `budget` bytes.
13427 fn paced_backend(pool: &Rc<RefCell<Pool>>, budget: usize) -> H2Shell<PacedSocket> {
13428 let (socket, _peer) = connected_socket();
13429 let backend = Rc::new(RefCell::new(crate::backends::Backend::new(
13430 "relink-backend",
13431 "127.0.0.1:2".parse().expect("backend address must parse"),
13432 None,
13433 None,
13434 None,
13435 )));
13436 let mut registry = crate::protocol::mux::BackendRegistry::default();
13437 let mut connection = H2Shell::new(
13438 Ulid::generate(),
13439 PacedSocket {
13440 stream: socket,
13441 budget,
13442 wire: Vec::new(),
13443 inbound: std::collections::VecDeque::new(),
13444 },
13445 Position::Client(
13446 "relink-cluster".into(),
13447 registry.id_for(&backend),
13448 BackendStatus::Connected,
13449 ),
13450 &mut PoolBufferSource::new(Rc::downgrade(pool)),
13451 H2FloodConfig::default(),
13452 H2ConnectionConfig::default(),
13453 Duration::from_secs(30),
13454 None,
13455 Duration::from_secs(30),
13456 Some((H2StreamId::Zero, 9)),
13457 Ready::READABLE | Ready::WRITABLE | Ready::HUP | Ready::ERROR,
13458 )
13459 .expect("a pool with free buffers must yield an H2 connection");
13460 connection.core.state = H2State::Header;
13461 connection
13462 }
13463
13464 /// Queue a GET request carrying `x-api-key: <key>` on `gid`'s request
13465 /// side, closed with END_STREAM, the way a relayed request reaches a
13466 /// backend's write pass. The custom field is indexed into the encoder's
13467 /// dynamic table, as a credential-like header would be.
13468 fn queue_backend_request(
13469 context: &mut Context<TestListener>,
13470 gid: GlobalStreamId,
13471 key: &'static [u8],
13472 ) {
13473 let kawa = &mut context.streams[gid].front;
13474 kawa.detached.status_line = kawa::StatusLine::Request {
13475 version: kawa::Version::V20,
13476 method: kawa::Store::Static(b"GET"),
13477 uri: kawa::Store::Static(b"/"),
13478 authority: kawa::Store::Static(b"example.com"),
13479 path: kawa::Store::Static(b"/"),
13480 };
13481 kawa.push_block(kawa::Block::StatusLine);
13482 kawa.push_block(kawa::Block::Header(kawa::Pair {
13483 key: kawa::Store::Static(b"x-api-key"),
13484 val: kawa::Store::Static(key),
13485 }));
13486 kawa.push_block(kawa::Block::Flags(kawa::Flags {
13487 end_body: false,
13488 end_chunk: false,
13489 end_header: true,
13490 end_stream: true,
13491 }));
13492 kawa.parsing_phase = kawa::ParsingPhase::Terminated;
13493 }
13494
13495 /// The #1632 starting point: a request linked to backend `backend`,
13496 /// whose write pass HPACK-encoded its HEADERS while the socket took
13497 /// nothing. Answers the request's global id and the exact bytes that
13498 /// connection encoded for it.
13499 fn park_an_encoded_request(
13500 backend: &mut H2Shell<PacedSocket>,
13501 context: &mut Context<TestListener>,
13502 router: &mut Router,
13503 ) -> (GlobalStreamId, Vec<u8>) {
13504 let gid = context
13505 .create_stream(Ulid::generate(), 1 << 16)
13506 .expect("test context must create a stream");
13507 queue_backend_request(context, gid, b"alice-secret");
13508 context.streams[gid].state = StreamState::Linked(mio::Token(1));
13509 assert!(backend.start_stream(gid, context), "premise: stream 1");
13510 backend.core.readiness.event.insert(Ready::WRITABLE);
13511 backend.writable(context, EndpointClient(router));
13512 let front = &context.streams[gid].front;
13513 assert!(
13514 backend.socket.wire.is_empty() && !front.out.is_empty() && !front.consumed,
13515 "premise: the request was encoded and parked unsent"
13516 );
13517 let encoded: Vec<u8> = front
13518 .as_io_slice()
13519 .iter()
13520 .flat_map(|slice| slice.iter().copied())
13521 .collect();
13522 (gid, encoded)
13523 }
13524
13525 /// RFC 9113 §6.9: a proxied stream has an independent send window on each
13526 /// connection. The client granted the maximum initial window on the
13527 /// frontend connection; the H2 backend then advertises
13528 /// `SETTINGS_INITIAL_WINDOW_SIZE = 2^31-1` too. Each value is within the
13529 /// §6.9.2 ceiling, so the backend connection must acknowledge it and send
13530 /// the request, not overflow a window it shares with the frontend leg and
13531 /// answer GOAWAY.
13532 ///
13533 /// TO SEE THIS RED: make `Stream::send_window_mut` and `Stream::split`
13534 /// answer `front_window` for `Position::Client` too. The backend then
13535 /// adds its delta to the client's 2^31-1: `the backend connection must
13536 /// not answer a valid initial window with GOAWAY`. Verified 2026-10-01
13537 /// (red on `3d3f077b`).
13538 #[test]
13539 fn a_backend_max_initial_window_is_independent_of_the_frontend_window() {
13540 let pool = make_pool_for_invariant_16();
13541 let mut backend = paced_backend(&pool, usize::MAX);
13542 let mut context = test_context(&pool);
13543 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
13544 // What a frontend connection whose client advertised 2^31-1 creates.
13545 let gid = context
13546 .create_stream(Ulid::generate(), FLOW_CONTROL_MAX_WINDOW)
13547 .expect("test context must create a stream");
13548 queue_backend_request(&mut context, gid, b"key");
13549 context.streams[gid].state = StreamState::Linked(mio::Token(1));
13550 assert!(backend.start_stream(gid, &mut context), "premise: stream 1");
13551
13552 let mut max_initial_window = vec![0, 4];
13553 max_initial_window.extend_from_slice(&FLOW_CONTROL_MAX_WINDOW.to_be_bytes());
13554 backend
13555 .socket
13556 .inbound
13557 .extend(orphan_frame(4, 0, 0, 6, &max_initial_window));
13558 for _ in 0..4 {
13559 backend.core.readiness.event.insert(Ready::READABLE);
13560 if backend.core.readiness.filter_interest().is_readable() {
13561 backend.readable(&mut context, EndpointClient(&mut router));
13562 }
13563 }
13564 backend.core.readiness.event.insert(Ready::WRITABLE);
13565 backend.writable(&mut context, EndpointClient(&mut router));
13566
13567 let frames = peer_frames(&backend.socket.wire).expect("whole frames on the wire");
13568 assert!(
13569 !frames.iter().any(|(kind, ..)| *kind == 7),
13570 "the backend connection must not answer a valid initial window with GOAWAY"
13571 );
13572 assert!(
13573 frames
13574 .iter()
13575 .any(|(kind, flags, ..)| *kind == 4 && *flags == 1),
13576 "the backend's SETTINGS are acknowledged"
13577 );
13578 assert!(
13579 frames.iter().any(|(kind, _, id, _)| *kind == 1 && *id == 1),
13580 "and the request leaves on stream 1"
13581 );
13582 }
13583
13584 /// A 4-octet WINDOW_UPDATE frame for `stream_id`.
13585 fn window_update_frame(stream_id: u32, increment: u32) -> Vec<u8> {
13586 orphan_frame(8, 0, stream_id, 4, &increment.to_be_bytes())
13587 }
13588
13589 /// RFC 9113 §6.9 on the backend leg: `start_stream` sizes the stream's
13590 /// send window from the initial window the BACKEND advertised (here
13591 /// 1000, already received on a pooled connection), whatever the frontend
13592 /// granted: larger (an H2 client at 2^31-1), the `1 << 16` an H1
13593 /// frontend seeds, or nothing. Taking the frontend's value would let a
13594 /// request body overrun the backend's window. The backend's WINDOW_UPDATE
13595 /// credits that leg only.
13596 ///
13597 /// TO SEE THIS RED: delete the `send_window_mut` assignment in
13598 /// `ConnectionH2::start_stream`: `the backend leg starts at the
13599 /// backend's window`, `left: 65535, right: 1000`. Verified 2026-10-01.
13600 #[test]
13601 fn a_backend_stream_window_is_sized_and_credited_by_the_backend_only() {
13602 const BACKEND_WINDOW: u32 = 1000;
13603 for front_window in [FLOW_CONTROL_MAX_WINDOW, 1 << 16, 0] {
13604 let pool = make_pool_for_invariant_16();
13605 let mut backend = paced_backend(&pool, usize::MAX);
13606 let mut context = test_context(&pool);
13607 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
13608 let read = |backend: &mut H2Shell<PacedSocket>,
13609 context: &mut Context<TestListener>,
13610 router: &mut Router| {
13611 for _ in 0..4 {
13612 backend.core.readiness.event.insert(Ready::READABLE);
13613 if backend.core.readiness.filter_interest().is_readable() {
13614 backend.readable(context, EndpointClient(router));
13615 }
13616 }
13617 };
13618 let mut initial_window = vec![0, 4];
13619 initial_window.extend_from_slice(&BACKEND_WINDOW.to_be_bytes());
13620 backend
13621 .socket
13622 .inbound
13623 .extend(orphan_frame(4, 0, 0, 6, &initial_window));
13624 read(&mut backend, &mut context, &mut router);
13625 let gid = context
13626 .create_stream(Ulid::generate(), front_window)
13627 .expect("test context must create a stream");
13628 queue_backend_request(&mut context, gid, b"key");
13629 context.streams[gid].state = StreamState::Linked(mio::Token(1));
13630 assert!(backend.start_stream(gid, &mut context), "premise: stream 1");
13631 assert_eq!(
13632 context.streams[gid].back_window, BACKEND_WINDOW as i32,
13633 "front window {front_window}: the backend leg starts at the backend's window"
13634 );
13635
13636 backend.socket.inbound.extend(window_update_frame(1, 500));
13637 read(&mut backend, &mut context, &mut router);
13638 let stream = &context.streams[gid];
13639 assert_eq!(
13640 stream.back_window,
13641 BACKEND_WINDOW as i32 + 500,
13642 "front window {front_window}: the backend's WINDOW_UPDATE credits its leg"
13643 );
13644 assert_eq!(
13645 stream.front_window,
13646 i32::try_from(front_window).unwrap(),
13647 "front window {front_window}: and leaves the frontend leg alone"
13648 );
13649 }
13650 }
13651
13652 /// Feed `inbound` to a frontend connection carrying streams 1 and 3,
13653 /// flush what it answers, and return the GOAWAY error codes it wrote.
13654 fn goaway_codes_after(
13655 connection: &mut H2Shell<PacedSocket>,
13656 context: &mut Context<TestListener>,
13657 router: &mut Router,
13658 inbound: &[u8],
13659 ) -> Vec<u32> {
13660 connection.socket.inbound.extend(inbound);
13661 for _ in 0..4 {
13662 connection.core.readiness.event.insert(Ready::READABLE);
13663 if connection.core.readiness.filter_interest().is_readable() {
13664 connection.readable(context, EndpointClient(router));
13665 }
13666 }
13667 connection.core.readiness.event.insert(Ready::WRITABLE);
13668 connection.writable(context, EndpointClient(router));
13669 peer_frames(&connection.socket.wire)
13670 .expect("whole frames on the wire")
13671 .iter()
13672 .filter(|(kind, ..)| *kind == 7)
13673 .map(|(_, _, _, payload)| {
13674 u32::from_be_bytes(payload[4..8].try_into().expect("GOAWAY error code"))
13675 })
13676 .collect()
13677 }
13678
13679 /// A SETTINGS frame carrying only `SETTINGS_INITIAL_WINDOW_SIZE = value`.
13680 fn initial_window_settings(value: u32) -> Vec<u8> {
13681 let mut payload = vec![0, 4];
13682 payload.extend_from_slice(&value.to_be_bytes());
13683 orphan_frame(4, 0, 0, 6, &payload)
13684 }
13685
13686 /// RFC 9113 §6.5.2: a `SETTINGS_INITIAL_WINDOW_SIZE` above 2^31-1 "MUST
13687 /// be treated as a connection error of type FLOW_CONTROL_ERROR".
13688 ///
13689 /// TO SEE THIS RED: answer the `SETTINGS_INITIAL_WINDOW_SIZE` arm of
13690 /// `ConnectionH2::handle_settings_frame` with `H2Error::ProtocolError`:
13691 /// `left: [1], right: [3]`. Verified 2026-10-01 (red on `c8779209`).
13692 #[test]
13693 fn an_initial_window_above_the_maximum_is_a_flow_control_error() {
13694 let (_pool, mut connection, mut context, mut router, _peer) = two_requests_read(usize::MAX);
13695 let codes = goaway_codes_after(
13696 &mut connection,
13697 &mut context,
13698 &mut router,
13699 &initial_window_settings(FLOW_CONTROL_MAX_WINDOW + 1),
13700 );
13701 assert_eq!(codes, vec![0x3], "GOAWAY(FLOW_CONTROL_ERROR)");
13702 }
13703
13704 /// RFC 9113 §6.9.2: a SETTINGS change that pushes a stream window past
13705 /// 2^31-1 is a connection error of type FLOW_CONTROL_ERROR. Stream 3's
13706 /// window already stands at 2^31-1; raising the initial window by one
13707 /// overflows it. The rejection is checked before any window moves, so
13708 /// stream 1, walked first, keeps its window.
13709 ///
13710 /// TO SEE THIS RED: in `ConnectionH2::update_initial_window_size`,
13711 /// apply each delta as it is checked, returning on the first overflow:
13712 /// `the rejected setting changes no stream window`, `left: 65536, right:
13713 /// 65535`. Verified 2026-10-01 (red on `c8779209`).
13714 #[test]
13715 fn a_settings_change_overflowing_a_stream_window_is_a_flow_control_error() {
13716 let (_pool, mut connection, mut context, mut router, _peer) = two_requests_read(usize::MAX);
13717 let gid = |connection: &H2Shell<PacedSocket>, id: u32| {
13718 *connection
13719 .core
13720 .stream_table
13721 .streams()
13722 .get(&id)
13723 .expect("the stream is open")
13724 };
13725 let (first, third) = (gid(&connection, 1), gid(&connection, 3));
13726 let mut inbound = orphan_frame(
13727 8,
13728 0,
13729 3,
13730 4,
13731 &(FLOW_CONTROL_MAX_WINDOW - DEFAULT_INITIAL_WINDOW_SIZE).to_be_bytes(),
13732 );
13733 inbound.extend(initial_window_settings(DEFAULT_INITIAL_WINDOW_SIZE + 1));
13734 let codes = goaway_codes_after(&mut connection, &mut context, &mut router, &inbound);
13735 assert_eq!(
13736 context.streams[third].front_window, FLOW_CONTROL_MAX_WINDOW as i32,
13737 "premise: stream 3's window stands at 2^31-1"
13738 );
13739 assert_eq!(codes, vec![0x3], "GOAWAY(FLOW_CONTROL_ERROR)");
13740 assert_eq!(
13741 context.streams[first].front_window, DEFAULT_INITIAL_WINDOW_SIZE as i32,
13742 "the rejected setting changes no stream window"
13743 );
13744 }
13745
13746 /// RFC 9113 §6.9 on the frontend leg: the client's WINDOW_UPDATE and
13747 /// SETTINGS_INITIAL_WINDOW_SIZE move the stream's frontend window only.
13748 #[test]
13749 fn a_frontend_window_update_credits_the_frontend_leg_only() {
13750 let (_pool, mut connection, mut context, mut router, _peer) = two_requests_read(usize::MAX);
13751 let first = *connection
13752 .core
13753 .stream_table
13754 .streams()
13755 .get(&1)
13756 .expect("the stream is open");
13757 context.streams[first].back_window = 1234;
13758 let mut initial_window = vec![0, 4];
13759 initial_window.extend_from_slice(&100_000u32.to_be_bytes());
13760 connection
13761 .socket
13762 .inbound
13763 .extend(window_update_frame(1, 1000));
13764 connection
13765 .socket
13766 .inbound
13767 .extend(orphan_frame(4, 0, 0, 6, &initial_window));
13768 for _ in 0..4 {
13769 connection.core.readiness.event.insert(Ready::READABLE);
13770 if connection.core.readiness.filter_interest().is_readable() {
13771 connection.readable(&mut context, EndpointClient(&mut router));
13772 }
13773 }
13774 let stream = &context.streams[first];
13775 assert_eq!(
13776 stream.front_window,
13777 100_000 + 1000,
13778 "the client's WINDOW_UPDATE and initial window move the frontend leg"
13779 );
13780 assert_eq!(stream.back_window, 1234, "and not the backend leg");
13781 }
13782
13783 /// sozu-proxy/sozu#1632, GOAWAY form: the backend refuses the stream
13784 /// (`last_stream_id` below it) after its HEADERS were encoded but not
13785 /// sent. It was not processed, but its request cannot be sent anywhere
13786 /// else: `front.out` holds this connection's frames and the blocks are
13787 /// gone. It must be refused (REFUSED_STREAM toward the client, RFC 9113
13788 /// §8.7), never queued for a re-link.
13789 ///
13790 /// TO SEE THIS RED: in [`ConnectionH2::handle_goaway_frame`], test
13791 /// `stream.front.consumed` alone. The stream is then re-linked with its
13792 /// stale frames: `a request encoded for this backend must not be re-linked
13793 /// after its GOAWAY`. Red on `d5161919`, verified 2026-09-28.
13794 #[test]
13795 fn a_request_encoded_for_a_backend_is_refused_on_goaway_not_relinked() {
13796 let pool = make_pool_for_invariant_16();
13797 let mut backend = paced_backend(&pool, 0);
13798 let mut context = test_context(&pool);
13799 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
13800 let (gid, _) = park_an_encoded_request(&mut backend, &mut context, &mut router);
13801
13802 // GOAWAY(last_stream_id = 0, NO_ERROR): stream 1 was not processed.
13803 backend
13804 .socket
13805 .inbound
13806 .extend(orphan_frame(7, 0, 0, 8, &[0; 8]));
13807 for _ in 0..4 {
13808 backend.core.readiness.event.insert(Ready::READABLE);
13809 if backend.core.readiness.filter_interest().is_readable() {
13810 backend.readable(&mut context, EndpointClient(&mut router));
13811 }
13812 }
13813
13814 assert!(
13815 !context.pending_links.contains(&gid),
13816 "a request encoded for this backend must not be re-linked after its GOAWAY"
13817 );
13818 let stream = &context.streams[gid];
13819 assert!(
13820 stream.context.status == Some(503) && stream.state == StreamState::Unlinked,
13821 "the client must be answered 503, got {:?} / {:?}",
13822 stream.context.status,
13823 stream.state
13824 );
13825 }
13826
13827 /// sozu-proxy/sozu#1632, H1 frontend form (found by the adversarial
13828 /// review of the first fix): behind a GOAWAY that refuses a request its
13829 /// H2 backend encoded but did not send, an H1 client must receive an
13830 /// error response, not silence. The first fix answered with
13831 /// `forcefully_terminate_answer(REFUSED_STREAM)`, an empty response kawa
13832 /// in `Error`, which `ConnectionH1::writable` cannot write: it withdraws
13833 /// WRITABLE and the client waits for its timeout.
13834 ///
13835 /// TO SEE THIS RED: in [`ConnectionH2::handle_goaway_frame`], answer the
13836 /// `front_bound_to_backend` branch with `forcefully_terminate_answer(..,
13837 /// H2Error::RefusedStream)` as the consumed branch does. The H1 client
13838 /// then reads nothing: `the H1 client must read an error response, got
13839 /// ""`. Verified 2026-09-28.
13840 #[test]
13841 fn an_h1_client_behind_a_goaway_refusing_its_encoded_request_gets_a_response() {
13842 use std::io::Read;
13843
13844 let pool = make_pool_for_invariant_16();
13845 let mut backend = paced_backend(&pool, 0);
13846 let mut context = test_context(&pool);
13847 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
13848 let (gid, _) = park_an_encoded_request(&mut backend, &mut context, &mut router);
13849 let (socket, mut client) = connected_socket();
13850 let mut frontend =
13851 Connection::new_h1_server(Ulid::generate(), socket, Duration::from_secs(30));
13852 assert_eq!(gid, 0, "premise: the H1 frontend serves slot 0");
13853
13854 backend
13855 .socket
13856 .inbound
13857 .extend(orphan_frame(7, 0, 0, 8, &[0; 8]));
13858 for _ in 0..4 {
13859 backend.core.readiness.event.insert(Ready::READABLE);
13860 if backend.core.readiness.filter_interest().is_readable() {
13861 backend.readable(
13862 &mut context,
13863 crate::protocol::mux::connection::EndpointServer(&mut frontend),
13864 );
13865 }
13866 }
13867 assert!(
13868 !context.pending_links.contains(&gid),
13869 "premise: the stream was refused, not re-linked"
13870 );
13871
13872 let mut received = Vec::new();
13873 for _ in 0..16 {
13874 frontend.readiness_mut().event.insert(Ready::WRITABLE);
13875 if frontend.readiness().filter_interest().is_writable() {
13876 frontend.writable(&mut context, EndpointClient(&mut router));
13877 }
13878 let mut buf = [0u8; 4096];
13879 if let Ok(n) = client.read(&mut buf) {
13880 received.extend_from_slice(&buf[..n]);
13881 }
13882 std::thread::yield_now();
13883 }
13884 assert!(
13885 received.starts_with(b"HTTP/1.1 503"),
13886 "the H1 client must read an error response, got {:?}",
13887 String::from_utf8_lossy(&received)
13888 );
13889 }
13890
13891 /// sozu-proxy/sozu#1632, connection-loss form: the backend connection
13892 /// goes away with the request encoded and parked, and
13893 /// `H2Shell::close` hands the stream to its frontend, whose
13894 /// `end_stream_decision` used to answer `Reconnect` on
13895 /// `!front.consumed`. The request must be answered with an error instead.
13896 ///
13897 /// TO SEE THIS RED: in `shared::end_stream_decision`, delete the
13898 /// `stream.front_bound_to_backend` branch. The stream is then re-linked:
13899 /// `a request encoded for a lost backend must not be re-linked`. Red on
13900 /// `d5161919`, verified 2026-09-28.
13901 #[test]
13902 fn a_request_encoded_for_a_lost_backend_is_answered_not_relinked() {
13903 let pool = make_pool_for_invariant_16();
13904 let mut backend = paced_backend(&pool, 0);
13905 let mut context = test_context(&pool);
13906 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
13907 let (gid, _) = park_an_encoded_request(&mut backend, &mut context, &mut router);
13908
13909 // What `H2Shell::close` does through `EndpointServer`: the frontend
13910 // ends the stream the lost backend carried.
13911 let (mut frontend, _peer) = test_h2_connection(&pool, None);
13912 frontend.core.end_stream(gid, &mut context);
13913
13914 assert!(
13915 !context.pending_links.contains(&gid),
13916 "a request encoded for a lost backend must not be re-linked"
13917 );
13918 assert_eq!(
13919 context.streams[gid].context.status,
13920 Some(502),
13921 "the client gets the answer a request that reached the backend gets"
13922 );
13923 }
13924
13925 /// sozu-proxy/sozu#1632, the property itself: no byte one backend
13926 /// connection's encoder produced may leave on another connection. Here
13927 /// the request encoded on backend A is offered to backend B, as a
13928 /// re-link would; B must refuse it and write nothing of A's.
13929 ///
13930 /// TO SEE THIS RED: in [`ConnectionH2::start_stream`], delete the
13931 /// `front.out` guard. B then sends A's frames — stream id 1 of A, a field
13932 /// block encoded against A's table: `backend B must not send what
13933 /// backend A encoded`. Red on `d5161919`, verified 2026-09-28.
13934 #[test]
13935 fn a_request_encoded_for_one_backend_never_reaches_another() {
13936 let pool = make_pool_for_invariant_16();
13937 let mut first = paced_backend(&pool, 0);
13938 let mut context = test_context(&pool);
13939 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
13940 let (gid, encoded) = park_an_encoded_request(&mut first, &mut context, &mut router);
13941 assert!(
13942 peer_frames(&encoded)
13943 .expect("whole frames")
13944 .iter()
13945 .any(|(kind, _, id, _)| *kind == 1 && *id == 1),
13946 "premise: backend A encoded a HEADERS frame for its stream 1"
13947 );
13948
13949 let mut second = paced_backend(&pool, usize::MAX);
13950 let started = second.start_stream(gid, &mut context);
13951 drive_both_ways(&mut second, &mut context, &mut router);
13952
13953 assert!(
13954 !second
13955 .socket
13956 .wire
13957 .windows(encoded.len())
13958 .any(|window| window == encoded.as_slice()),
13959 "backend B must not send what backend A encoded: {:?}",
13960 peer_frames(&second.socket.wire)
13961 );
13962 assert!(
13963 !started,
13964 "backend B must refuse a request encoded for another connection"
13965 );
13966 }
13967
13968 /// Queue on `gid`'s response side a 200 whose field block is `fields`,
13969 /// closed with END_STREAM, the way a relayed response reaches the write
13970 /// pass.
13971 fn queue_response(
13972 connection: &mut H2Shell<PacedSocket>,
13973 context: &mut Context<TestListener>,
13974 gid: GlobalStreamId,
13975 fields: Vec<(&'static [u8], kawa::Store)>,
13976 ) {
13977 let stream = &mut context.streams[gid];
13978 let kawa = &mut stream.back;
13979 kawa.detached.status_line = kawa::StatusLine::Response {
13980 version: kawa::Version::V20,
13981 code: 200,
13982 status: kawa::Store::Static(b"200"),
13983 reason: kawa::Store::Empty,
13984 };
13985 kawa.push_block(kawa::Block::StatusLine);
13986 for (name, value) in fields {
13987 kawa.push_block(kawa::Block::Header(kawa::Pair {
13988 key: kawa::Store::Static(name),
13989 val: value,
13990 }));
13991 }
13992 kawa.push_block(kawa::Block::Flags(kawa::Flags {
13993 end_body: false,
13994 end_chunk: false,
13995 end_header: true,
13996 end_stream: true,
13997 }));
13998 kawa.parsing_phase = kawa::ParsingPhase::Terminated;
13999 stream.state = StreamState::Unlinked;
14000 connection.core.readiness.arm_writable();
14001 }
14002
14003 /// A block dropped for exceeding `MAX_HEADER_LIST_SIZE` re-arms a table
14004 /// reset that must outlive the write pass when no other block of that
14005 /// pass carries it (sozu-proxy/sozu#1627, path 2). Stream 1's response
14006 /// inserts `x-a: 1`, then an `etag` value past the limit aborts it into a
14007 /// RST_STREAM, and it is the only header block of its pass. Stream 3's
14008 /// response, answered afterwards, encodes `x-a: 1` again, and the peer
14009 /// must read it: its block has to open with the reset.
14010 ///
14011 /// TO SEE THIS RED: in `ConnectionH2::poll_write_target`'s
14012 /// `H2WritePhase::End` arm, restore the pre-image
14013 /// `if converter_pass.size_update_emitted() { self.pending_table_size_update
14014 /// = None; }` instead of taking `converter_pass.pending_table_size_update()`.
14015 /// The reset is then lost with the pass: `the reset is still owed after
14016 /// the pass`, `left: None, right: Some(4096)`. Verified 2026-09-27.
14017 #[test]
14018 fn an_oversized_block_reset_outlives_the_pass_that_dropped_it() {
14019 let (_pool, mut connection, mut context, mut router, _peer) = two_requests_read(usize::MAX);
14020 let gid = |connection: &H2Shell<PacedSocket>, id: u32| {
14021 *connection
14022 .core
14023 .stream_table
14024 .streams()
14025 .get(&id)
14026 .expect("the stream is open")
14027 };
14028 let (first, third) = (gid(&connection, 1), gid(&connection, 3));
14029 let oversized = vec![b'v'; MAX_HEADER_LIST_SIZE + 1];
14030 queue_response(
14031 &mut connection,
14032 &mut context,
14033 first,
14034 vec![
14035 (b"x-a", kawa::Store::Static(b"1")),
14036 (b"etag", kawa::Store::from_slice(&oversized)),
14037 ],
14038 );
14039 drive_both_ways(&mut connection, &mut context, &mut router);
14040 assert!(
14041 peer_frames(&connection.socket.wire)
14042 .unwrap()
14043 .iter()
14044 .any(|(kind, _, id, _)| *kind == 3 && *id == 1),
14045 "premise: stream 1 was reset without HEADERS"
14046 );
14047 assert_eq!(
14048 connection.core.pending_table_size_update,
14049 Some(4096),
14050 "the reset is still owed after the pass"
14051 );
14052 queue_response(
14053 &mut connection,
14054 &mut context,
14055 third,
14056 vec![(b"x-a", kawa::Store::Static(b"1"))],
14057 );
14058 drive_both_ways(&mut connection, &mut context, &mut router);
14059 let (seen, status) = peer_header_blocks(&connection.socket.wire);
14060 assert_eq!(
14061 seen,
14062 vec![(3, true)],
14063 "the peer must decode stream 3's block with its own table"
14064 );
14065 assert_eq!(status, vec![b"200".to_vec()]);
14066 }
14067
14068 /// A response body 1000 octets past the default window, so one prepare
14069 /// takes the whole connection window and leaves the rest in the blocks.
14070 /// A `Store::Static`, because kawa 0.7.1's `Store::Alloc` arm of
14071 /// `Store::consume` subtracts the length before it adds the index back,
14072 /// which overflows in a debug build once the converter has split the
14073 /// chunk (CleverCloud/kawa#22).
14074 static WINDOW_SIZED_BODY: [u8; DEFAULT_INITIAL_WINDOW_SIZE as usize + 1000] =
14075 [b'b'; DEFAULT_INITIAL_WINDOW_SIZE as usize + 1000];
14076
14077 /// Queue on `gid`'s response side a 200 carrying `body`, closed with
14078 /// END_STREAM, the way a relayed response with a payload reaches the
14079 /// write pass.
14080 fn queue_response_with_body(
14081 connection: &mut H2Shell<PacedSocket>,
14082 context: &mut Context<TestListener>,
14083 gid: GlobalStreamId,
14084 body: &'static [u8],
14085 ) {
14086 let stream = &mut context.streams[gid];
14087 let kawa = &mut stream.back;
14088 kawa.detached.status_line = kawa::StatusLine::Response {
14089 version: kawa::Version::V20,
14090 code: 200,
14091 status: kawa::Store::Static(b"200"),
14092 reason: kawa::Store::Empty,
14093 };
14094 kawa.push_block(kawa::Block::StatusLine);
14095 kawa.push_block(kawa::Block::Flags(kawa::Flags {
14096 end_body: false,
14097 end_chunk: false,
14098 end_header: true,
14099 end_stream: false,
14100 }));
14101 kawa.push_block(kawa::Block::Chunk(kawa::Chunk {
14102 data: kawa::Store::Static(body),
14103 }));
14104 kawa.push_block(kawa::Block::Flags(kawa::Flags {
14105 end_body: true,
14106 end_chunk: false,
14107 end_header: false,
14108 end_stream: true,
14109 }));
14110 kawa.parsing_phase = kawa::ParsingPhase::Terminated;
14111 stream.state = StreamState::Unlinked;
14112 connection.core.readiness.arm_writable();
14113 }
14114
14115 /// The DATA payload octets `wire` carries for `stream_id`.
14116 fn data_on_wire(wire: &[u8], stream_id: u32) -> usize {
14117 peer_frames(wire)
14118 .unwrap()
14119 .iter()
14120 .filter(|(kind, _, id, _)| *kind == 0 && *id == stream_id)
14121 .map(|(_, _, _, payload)| payload.len())
14122 .sum()
14123 }
14124
14125 /// Stream 1's response body fills the whole connection send window in
14126 /// one prepare, the socket takes `budget` octets, and the stream is
14127 /// parked with the rest still queued. Its removal drops what the wire
14128 /// has not started.
14129 fn park_a_window_sized_body_on_stream_1(
14130 budget: usize,
14131 ) -> (
14132 H2Shell<PacedSocket>,
14133 Context<TestListener>,
14134 Router,
14135 GlobalStreamId,
14136 GlobalStreamId,
14137 ) {
14138 let (_pool, mut connection, mut context, mut router, _peer) = two_requests_read(budget);
14139 let gid = |connection: &H2Shell<PacedSocket>, id: u32| {
14140 *connection
14141 .core
14142 .stream_table
14143 .streams()
14144 .get(&id)
14145 .expect("the stream is open")
14146 };
14147 let (first, third) = (gid(&connection, 1), gid(&connection, 3));
14148 queue_response_with_body(&mut connection, &mut context, first, &WINDOW_SIZED_BODY);
14149 connection.core.readiness.event.insert(Ready::WRITABLE);
14150 connection.writable(&mut context, EndpointClient(&mut router));
14151 assert!(
14152 connection.socket.wire.len() == budget
14153 && connection.core.stream_table.expect_write().is_some(),
14154 "premise: stream 1's DATA was prepared and parked, the socket took {budget} octets"
14155 );
14156 assert_eq!(
14157 connection.core.flow_control.window(),
14158 0,
14159 "premise: preparing stream 1's body took the whole connection window"
14160 );
14161 (connection, context, router, first, third)
14162 }
14163
14164 /// RFC 9113 §6.9: the connection send window accounts for the DATA that
14165 /// was SENT. DATA prepared on stream 1 and dropped unsent when the peer
14166 /// resets it never reaches the peer, which will therefore never return
14167 /// that credit, so the connection must take it back itself; otherwise
14168 /// the window stays at zero for the life of the connection and stream 3's
14169 /// body never leaves (sozu-proxy/sozu#1641).
14170 ///
14171 /// TO SEE THIS RED: in `ConnectionH2::remove_dead_stream`, delete the
14172 /// `self.refund_parked_data(parked_data)` call. The removal then gives
14173 /// nothing back: `the removal itself gives the credit back`, `left: 0,
14174 /// right: 65535`. Without the fix at all, stream 3 also sends nothing
14175 /// (`left: 0, right: 1000`). Delete the `self.readiness.arm_writable()`
14176 /// in `ConnectionH2::refund_parked_data` instead: `and re-arms WRITABLE`
14177 /// fails. Verified 2026-09-28 (red on `5db53c60`).
14178 #[test]
14179 fn data_dropped_with_its_parked_stream_gives_the_connection_window_back() {
14180 let (mut connection, mut context, mut router, _first, third) =
14181 park_a_window_sized_body_on_stream_1(0);
14182 // A zero-progress pass may release WRITABLE; only the refund may
14183 // bring it back here.
14184 connection.core.readiness.interest.remove(Ready::WRITABLE);
14185 connection.socket.inbound.extend(RST_STREAM_1);
14186 for _ in 0..4 {
14187 connection.core.readiness.event.insert(Ready::READABLE);
14188 connection.readable(&mut context, EndpointClient(&mut router));
14189 }
14190 assert!(
14191 !connection.core.stream_table.streams().contains_key(&1),
14192 "premise: the RST removed stream 1"
14193 );
14194 assert_eq!(
14195 connection.core.flow_control.window(),
14196 DEFAULT_INITIAL_WINDOW_SIZE as i32,
14197 "the removal itself gives the credit back, before any write pass"
14198 );
14199 assert!(
14200 connection.core.readiness.interest.is_writable(),
14201 "and re-arms WRITABLE for the streams the closed window starved"
14202 );
14203 connection.socket.budget = usize::MAX;
14204 queue_response_with_body(&mut connection, &mut context, third, &[b'c'; 1000]);
14205 drive_both_ways(&mut connection, &mut context, &mut router);
14206 let wire = &connection.socket.wire;
14207 assert_eq!(data_on_wire(wire, 1), 0, "stream 1's DATA never left");
14208 assert_eq!(
14209 data_on_wire(wire, 3),
14210 1000,
14211 "stream 3's body must not wait on credit spent on dropped DATA"
14212 );
14213 assert_eq!(
14214 i64::from(connection.core.flow_control.window())
14215 + (data_on_wire(wire, 1) + data_on_wire(wire, 3)) as i64,
14216 i64::from(DEFAULT_INITIAL_WINDOW_SIZE),
14217 "the window is debited for the DATA octets sent, and only those"
14218 );
14219 }
14220
14221 /// The same drop without a removal: a forced termination clears the
14222 /// parked stream's queue and keeps the stream, which then sends its
14223 /// RST_STREAM. Both the connection's and the stream's credit for the
14224 /// dropped DATA come back (sozu-proxy/sozu#1641).
14225 ///
14226 /// TO SEE THIS RED: in `ConnectionH2::poll_write_target`'s
14227 /// `H2WritePhase::Start` arm, delete the `self.refund_parked_data(dropped)`
14228 /// call: `the connection window is back where it was before the
14229 /// prepare`, `left: 0, right: 65535`. Delete the `*parts.window` refund
14230 /// beside it instead: `the stream's window is back where it was before
14231 /// the prepare`, `left: 0, right: 65535`. Verified 2026-09-28 (red on
14232 /// `5db53c60`).
14233 #[test]
14234 fn data_dropped_from_a_live_parked_stream_gives_both_windows_back() {
14235 let (mut connection, mut context, mut router, first, _third) =
14236 park_a_window_sized_body_on_stream_1(0);
14237 crate::protocol::mux::answers::forcefully_terminate_answer(
14238 &mut context.streams[first],
14239 &mut connection.core.readiness,
14240 H2Error::InternalError,
14241 );
14242 connection.socket.budget = usize::MAX;
14243 connection.core.readiness.event.insert(Ready::WRITABLE);
14244 connection.writable(&mut context, EndpointClient(&mut router));
14245 assert_eq!(
14246 context.streams[first].front_window, DEFAULT_INITIAL_WINDOW_SIZE as i32,
14247 "the stream's window is back where it was before the prepare"
14248 );
14249 drive_both_ways(&mut connection, &mut context, &mut router);
14250 assert_eq!(
14251 data_on_wire(&connection.socket.wire, 1),
14252 0,
14253 "premise: stream 1's DATA never left"
14254 );
14255 assert_eq!(
14256 connection.core.flow_control.window(),
14257 DEFAULT_INITIAL_WINDOW_SIZE as i32,
14258 "the connection window is back where it was before the prepare"
14259 );
14260 }
14261
14262 /// The peer grows the connection window to 2^31-1 while stream 1's
14263 /// 65535 prepared octets sit parked, then resets the stream. The window
14264 /// that really stands is 2^31-1 plus those octets, past the RFC 9113
14265 /// §6.9.1 ceiling, and the refund is where that shows: the connection
14266 /// answers GOAWAY(FLOW_CONTROL_ERROR), as it does for an overflowing
14267 /// WINDOW_UPDATE, instead of asserting on what the peer sent.
14268 ///
14269 /// TO SEE THIS RED: in `H2FlowControl::refund_send_window`, put back a
14270 /// `debug_assert!(false, ..)` on the `checked_add` overflow: the test
14271 /// panics on `a refund of consumed credit cannot overflow the send
14272 /// window`, which is how `08730c9a` behaved. In
14273 /// `ConnectionH2::refund_parked_data`, answer `None` on the overflow
14274 /// instead of the GOAWAY: `exactly one GOAWAY`, `left: 0, right: 1`.
14275 /// Verified 2026-09-28.
14276 #[test]
14277 fn a_refund_the_peer_pushed_past_the_window_ceiling_is_a_flow_control_error() {
14278 let (mut connection, mut context, mut router, _first, _third) =
14279 park_a_window_sized_body_on_stream_1(0);
14280 // WINDOW_UPDATE(stream 0, 2^31-1), then RST_STREAM(1, CANCEL).
14281 connection
14282 .socket
14283 .inbound
14284 .extend(b"\x00\x00\x04\x08\x00\x00\x00\x00\x00\x7f\xff\xff\xff");
14285 connection.socket.inbound.extend(RST_STREAM_1);
14286 for _ in 0..4 {
14287 connection.core.readiness.event.insert(Ready::READABLE);
14288 connection.readable(&mut context, EndpointClient(&mut router));
14289 }
14290 assert_eq!(
14291 connection.core.flow_control.window(),
14292 i32::MAX,
14293 "premise: the peer's WINDOW_UPDATE was accepted, and the refund not applied"
14294 );
14295 connection.socket.budget = usize::MAX;
14296 drive_both_ways(&mut connection, &mut context, &mut router);
14297 let goaways: Vec<Vec<u8>> = peer_frames(&connection.socket.wire)
14298 .unwrap()
14299 .into_iter()
14300 .filter(|(kind, _, _, _)| *kind == 7)
14301 .map(|(_, _, _, payload)| payload)
14302 .collect();
14303 assert_eq!(goaways.len(), 1, "exactly one GOAWAY");
14304 assert_eq!(
14305 &goaways[0][4..8],
14306 &3u32.to_be_bytes(),
14307 "the GOAWAY carries FLOW_CONTROL_ERROR"
14308 );
14309 }
14310
14311 /// A second write pass that is stalled too resumes the parked stream,
14312 /// writes nothing and parks it again: every prepared DATA octet is still
14313 /// queued, so nothing is given back and both windows stay at zero
14314 /// (sozu-proxy/sozu#1641). Only DATA no longer queued is refunded.
14315 ///
14316 /// TO SEE THIS RED: in `ConnectionH2::poll_write_target`'s
14317 /// `H2WritePhase::Start` arm, refund `parked_data` instead of
14318 /// `parked_data - queued`: `left: 65535, right: 0`. Verified 2026-09-28.
14319 #[test]
14320 fn data_still_parked_after_a_second_stalled_pass_is_not_given_back() {
14321 let (mut connection, mut context, mut router, first, _third) =
14322 park_a_window_sized_body_on_stream_1(0);
14323 connection.core.readiness.event.insert(Ready::WRITABLE);
14324 connection.writable(&mut context, EndpointClient(&mut router));
14325 assert!(
14326 connection.socket.wire.is_empty()
14327 && connection.core.stream_table.expect_write().is_some(),
14328 "premise: the second pass resumed nothing and kept the park"
14329 );
14330 assert_eq!(
14331 connection.core.flow_control.window(),
14332 0,
14333 "the connection window still owes every queued DATA octet"
14334 );
14335 assert_eq!(
14336 context.streams[first].front_window, 0,
14337 "and so does the stream's"
14338 );
14339 }
14340
14341 /// A write that cuts stream 1's first DATA frame hands the rest of that
14342 /// frame to the ordered output queue, which sends it whole even after
14343 /// the stream is gone (sozu-proxy/sozu#1625). Only the frames behind it,
14344 /// which the wire never started, are given back: the window still
14345 /// accounts for exactly the DATA octets sent (sozu-proxy/sozu#1641).
14346 ///
14347 /// TO SEE THIS RED: in `ConnectionH2::remove_dead_stream`, give back the
14348 /// whole prepare instead of the parked DATA
14349 /// (`self.refund_parked_data(DEFAULT_INITIAL_WINDOW_SIZE as i32)`). The
14350 /// window then exceeds the peer's grant by the cut frame's payload:
14351 /// `left: 81919, right: 65535`. Without the fix at all it falls short by
14352 /// the dropped frames instead: `left: 16384, right: 65535`. Verified
14353 /// 2026-09-28 (red on `5db53c60`).
14354 #[test]
14355 fn data_a_partial_write_started_is_sent_whole_and_not_given_back() {
14356 // A 10-octet HEADERS (`:status: 200`), then 109 octets of the first
14357 // DATA frame: its header and 100 octets of payload.
14358 let budget = 10 + 9 + 100;
14359 let (mut connection, mut context, mut router, _first, _third) =
14360 park_a_window_sized_body_on_stream_1(budget);
14361 connection.socket.inbound.extend(RST_STREAM_1);
14362 for _ in 0..4 {
14363 connection.core.readiness.event.insert(Ready::READABLE);
14364 connection.readable(&mut context, EndpointClient(&mut router));
14365 }
14366 assert!(
14367 !connection.core.stream_table.streams().contains_key(&1),
14368 "premise: the RST removed stream 1"
14369 );
14370 connection.socket.budget = usize::MAX;
14371 drive_both_ways(&mut connection, &mut context, &mut router);
14372 let sent = data_on_wire(&connection.socket.wire, 1);
14373 assert_eq!(
14374 sent, 16_384,
14375 "the cut frame left whole, and nothing after it"
14376 );
14377 assert_eq!(
14378 i64::from(connection.core.flow_control.window()) + sent as i64,
14379 i64::from(DEFAULT_INITIAL_WINDOW_SIZE),
14380 "the window is debited for the DATA octets sent, and only those"
14381 );
14382 }
14383
14384 /// An H1 backend answers chunked with `Connection: close`, sends the
14385 /// last chunk and a first trailer line, then closes before the final
14386 /// CRLF. The trailer block can never be completed, and the converter
14387 /// defers a field block until its closing flags are queued
14388 /// (sozu-proxy/sozu#1627). `ConnectionH2::end_stream`'s
14389 /// `EndStreamAction::CloseDelimited` arm must not leave the stream
14390 /// waiting on that block: the chunked body was cut short, so it ends
14391 /// with RST_STREAM, exactly as `ConnectionH1::terminate_close_delimited`
14392 /// (`lib/src/protocol/mux/h1.rs`) does, and the unfinished trailer is
14393 /// dropped unencoded, so the next response still decodes on the peer.
14394 ///
14395 /// Red on `d12c3299`, which deferred the trailer with no way out: the
14396 /// stream stayed open with its trailer queued forever, `stream 1 must end
14397 /// with RST_STREAM`, frames `[(1, 0), (0, 0)]`. TO SEE THIS RED now: in
14398 /// `ConnectionH2::end_stream`, delete the `EndStreamAction::CloseDelimited`
14399 /// arm guarded by `body_size != kawa::BodySize::Empty`. The truncated
14400 /// response then ends silently: frames `[(1, 0), (0, 0), (0, 1)]`.
14401 /// Verified 2026-09-27.
14402 #[test]
14403 fn a_chunked_response_closed_inside_its_trailers_ends_with_a_reset() {
14404 let (_pool, mut connection, mut context, mut router, _peer) = two_requests_read(usize::MAX);
14405 let gid = |connection: &H2Shell<PacedSocket>, id: u32| {
14406 *connection
14407 .core
14408 .stream_table
14409 .streams()
14410 .get(&id)
14411 .expect("the stream is open")
14412 };
14413 let (first, third) = (gid(&connection, 1), gid(&connection, 3));
14414 {
14415 let stream = &mut context.streams[first];
14416 stream.context.keep_alive_backend = false;
14417 let bytes: &[u8] = b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n3\r\nabc\r\n0\r\nx-checksum: abc\r\n";
14418 let kawa = &mut stream.back;
14419 kawa.storage.space()[..bytes.len()].copy_from_slice(bytes);
14420 kawa.storage.fill(bytes.len());
14421 kawa::h1::parse(kawa, &mut kawa::h1::NoCallbacks);
14422 assert_eq!(
14423 kawa.parsing_phase,
14424 kawa::ParsingPhase::Trailers,
14425 "premise: the backend stopped inside the trailers"
14426 );
14427 stream.state = StreamState::Unlinked;
14428 }
14429 connection.core.readiness.arm_writable();
14430 drive_both_ways(&mut connection, &mut context, &mut router);
14431 connection.core.end_stream(first, &mut context);
14432 drive_both_ways(&mut connection, &mut context, &mut router);
14433
14434 let frames: Vec<(u8, u8)> = peer_frames(&connection.socket.wire)
14435 .unwrap()
14436 .iter()
14437 .filter(|(_, _, id, _)| *id == 1)
14438 .map(|(kind, flags, _, _)| (*kind, flags & parser::FLAG_END_STREAM))
14439 .collect();
14440 assert_eq!(
14441 frames.last().map(|(kind, _)| *kind),
14442 Some(0x3),
14443 "stream 1 must end with RST_STREAM, frames {frames:?}"
14444 );
14445 assert!(
14446 !frames.iter().any(|(_, end_stream)| *end_stream != 0),
14447 "a truncated chunked body never ends cleanly: {frames:?}"
14448 );
14449 assert!(
14450 !connection.core.stream_table.streams().contains_key(&1),
14451 "stream 1 is retired"
14452 );
14453
14454 queue_response(
14455 &mut connection,
14456 &mut context,
14457 third,
14458 vec![(b"x-checksum", kawa::Store::Static(b"abc"))],
14459 );
14460 drive_both_ways(&mut connection, &mut context, &mut router);
14461 let (seen, status) = peer_header_blocks(&connection.socket.wire);
14462 assert_eq!(
14463 seen,
14464 vec![(1, true), (3, true)],
14465 "every block the peer reads decodes with its own table"
14466 );
14467 assert_eq!(status, vec![b"200".to_vec(), b"200".to_vec()]);
14468 }
14469
14470 /// An H1 backend answers `Content-Length: 10` with `Connection: close`,
14471 /// sends 4 body bytes, and its connection goes away without the EOF read
14472 /// path ending the body (a socket error or a backend timeout), so
14473 /// `ConnectionH2::end_stream` meets the response still in its body phase.
14474 /// Its `EndStreamAction::CloseDelimited` arm must end the stream with
14475 /// RST_STREAM: the body is 6 bytes short of its `content-length`, and
14476 /// only a body with neither `Content-Length` nor chunked coding is
14477 /// delimited by the close (RFC 9112 §6.3; RFC 9113 §8.1.1).
14478 ///
14479 /// Red on `d5161919`, whose guard covered chunked only: stream 1 ended
14480 /// with DATA carrying END_STREAM (sozu-proxy/sozu#1633).
14481 #[test]
14482 fn a_content_length_response_cut_short_ends_with_a_reset() {
14483 let (_pool, mut connection, mut context, mut router, _peer) = two_requests_read(usize::MAX);
14484 let first = *connection
14485 .core
14486 .stream_table
14487 .streams()
14488 .get(&1)
14489 .expect("the stream is open");
14490 {
14491 let stream = &mut context.streams[first];
14492 stream.context.keep_alive_backend = false;
14493 let bytes: &[u8] = b"HTTP/1.1 200 OK\r\nContent-Length: 10\r\n\r\nabcd";
14494 let kawa = &mut stream.back;
14495 kawa.storage.space()[..bytes.len()].copy_from_slice(bytes);
14496 kawa.storage.fill(bytes.len());
14497 kawa::h1::parse(kawa, &mut kawa::h1::NoCallbacks);
14498 assert!(
14499 kawa.is_main_phase() && !kawa.is_terminated() && kawa.expects == 6,
14500 "premise: the body is 6 bytes short, {:?}",
14501 kawa.parsing_phase
14502 );
14503 stream.state = StreamState::Unlinked;
14504 }
14505 connection.core.readiness.arm_writable();
14506 drive_both_ways(&mut connection, &mut context, &mut router);
14507 connection.core.end_stream(first, &mut context);
14508 drive_both_ways(&mut connection, &mut context, &mut router);
14509
14510 let frames: Vec<(u8, u8)> = peer_frames(&connection.socket.wire)
14511 .unwrap()
14512 .iter()
14513 .filter(|(_, _, id, _)| *id == 1)
14514 .map(|(kind, flags, _, _)| (*kind, flags & parser::FLAG_END_STREAM))
14515 .collect();
14516 assert_eq!(
14517 frames.last().map(|(kind, _)| *kind),
14518 Some(0x3),
14519 "stream 1 must end with RST_STREAM, frames {frames:?}"
14520 );
14521 assert!(
14522 !frames.iter().any(|(_, end_stream)| *end_stream != 0),
14523 "a truncated Content-Length body never ends cleanly: {frames:?}"
14524 );
14525 }
14526
14527 /// The same block dropped while its stream stays parked:
14528 /// `forcefully_terminate_answer` (a backend that closed mid-response)
14529 /// clears the parked stream's `kawa.out`, which held the encoded block.
14530 /// Stream 1 then ends with a RST_STREAM, and stream 3's block must still
14531 /// decode on the peer (sozu-proxy/sozu#1627).
14532 ///
14533 /// TO SEE THIS RED: in `ConnectionH2::poll_write_target`'s
14534 /// `H2WritePhase::Start` arm, append `&& false` to the `.is_empty()` that
14535 /// judges a park still in place. The peer then fails on stream 3's block:
14536 /// `left: [(3, false)], right: [(3, true)]`, and this is the only test
14537 /// that fails. Verified 2026-09-27.
14538 #[test]
14539 fn a_header_block_cleared_from_a_parked_stream_keeps_the_peer_table_in_sync() {
14540 let (seen, status) =
14541 peer_reads_after_a_dropped_parked_block(|connection, context, _router| {
14542 let gid = *connection
14543 .core
14544 .stream_table
14545 .streams()
14546 .get(&1)
14547 .expect("stream 1 is open");
14548 forcefully_terminate_answer(
14549 &mut context.streams[gid],
14550 &mut connection.core.readiness,
14551 H2Error::InternalError,
14552 );
14553 assert!(
14554 context.streams[gid].back.out.is_empty()
14555 && connection.core.stream_table.expect_write().is_some(),
14556 "premise: stream 1 stays parked with its block cleared"
14557 );
14558 });
14559 assert_eq!(
14560 seen,
14561 vec![(3, true)],
14562 "the peer must decode stream 3's block with its own table"
14563 );
14564 assert_eq!(status, vec![b"404".to_vec()], "and read what was encoded");
14565 }
14566
14567 /// Resume-path bytes are not pass progress.
14568 ///
14569 /// `finalize_write` reads `bytes_written_this_pass > 0` as
14570 /// `made_progress`, and `made_progress && any_pending_back()` is LIFECYCLE
14571 /// §9 invariant 16: it RETAINS `Ready::WRITABLE` because a pass that moved
14572 /// bytes and left blocks behind is a voluntary scheduler yield that will
14573 /// resume itself. The resume path's drain is not that. It happens in
14574 /// `H2WritePhase::Resume`, before the scheduler pass opens at all; it is a
14575 /// socket-backpressure catch-up rather than a
14576 /// yield, and the stranded blocks it leaves belong to a stream the
14577 /// converter never reached this pass. Counting it as progress would keep
14578 /// WRITABLE armed on a connection whose scheduler pass did nothing, and
14579 /// the next tick would find the same nothing — a spin, not a resume.
14580 ///
14581 /// So `resume_bytes` is deliberately NOT accumulated into
14582 /// `total_bytes_written`: only the main loop's `stream_bytes` is. With a
14583 /// drained resume and a zero-byte scheduler pass, the correct answer is
14584 /// `FinalizeAction::Quiesce`, which withdraws `Ready::WRITABLE` and waits
14585 /// for an external trigger.
14586 ///
14587 /// TO SEE THIS RED: in `begin_scheduler_pass`, seed the pass total from
14588 /// the resume counter — insert `pass.total_bytes_written =
14589 /// pass.resume_bytes;` immediately before
14590 /// `pass.adopt_scheduler_pass(converter_pass, order, census);`, which is
14591 /// where the pre-image seeded `let mut total_bytes_written: usize = 0;`
14592 /// ahead of its per-stream loop. Both counters are fields of
14593 /// `H2WritePass` and keeping them apart is the whole point, so the shape
14594 /// a refactor unifying the two flush paths would reach for is exactly this
14595 /// one statement. Measured: this test fails on its own first
14596 /// assertion, `a pass whose scheduler loop wrote nothing must withdraw
14597 /// WRITABLE interest`, with `left: Writable | Error | Hup, right: Error |
14598 /// Hup`, and it is the ONLY failure in the suite.
14599 /// No other test reaches the resume path at all, which is why the blast
14600 /// radius is one.
14601 #[test]
14602 fn resume_path_bytes_do_not_make_the_pass_progress() {
14603 let pool = make_pool_for_invariant_16();
14604 let (mut connection, mut context, gid, _peer) = writable_fixture(
14605 &pool,
14606 &[FIRST_BLOCK, SECOND_BLOCK],
14607 &[(TOTAL_QUEUED, SocketResult::Continue)],
14608 );
14609 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
14610
14611 connection
14612 .core
14613 .stream_table
14614 .set_expect_write(Some(H2StreamId::Other {
14615 id: REGISTERED_STREAM_ID,
14616 gid,
14617 }));
14618 // A pending back-buffer, which is the OTHER half of invariant 16's
14619 // condition. The flush drains `out` and never touches
14620 // `blocks`, so this block survives the resume and makes
14621 // `any_stream_has_pending_back` answer true — leaving `made_progress`
14622 // as the single input that decides Quiesce against RetainPendingBack.
14623 context.streams[gid]
14624 .back
14625 .blocks
14626 .push_back(kawa::Block::StatusLine);
14627 assert_prepare_gate_is_shut(&context, gid);
14628 assert!(
14629 connection.core.readiness.interest.is_writable(),
14630 "premise: WRITABLE interest must start armed, or the withdrawal \
14631 below is unobservable, got {:?}",
14632 connection.core.readiness
14633 );
14634
14635 let result = connection.writable(&mut context, EndpointClient(&mut router));
14636
14637 assert_eq!(
14638 connection.core.readiness.interest,
14639 Ready::HUP | Ready::ERROR,
14640 "a pass whose scheduler loop wrote nothing must withdraw WRITABLE \
14641 interest: the resume path's drain is socket catch-up, not the \
14642 voluntary yield invariant 16 retains the bit for"
14643 );
14644 assert_eq!(
14645 context.streams[gid].metrics.bout, TOTAL_QUEUED,
14646 "premise: the resume path really did move every queued byte — \
14647 without that this test would assert Quiesce over a pass that \
14648 simply had nothing to write"
14649 );
14650 assert!(
14651 !context.streams[gid].back.blocks.is_empty(),
14652 "premise: the pending back-buffer must survive the pass, or \
14653 any_stream_has_pending_back answers false and RetainPendingBack \
14654 is unreachable whatever made_progress says"
14655 );
14656 assert_eq!(
14657 connection.core.stream_table.expect_write(),
14658 None,
14659 "a drained resume clears the park before the scheduler pass"
14660 );
14661 assert_eq!(
14662 connection.socket.vectored_calls, 1,
14663 "the scheduler pass must find nothing to write: the resume drained \
14664 `out`, and the prepare gate is shut"
14665 );
14666 assert!(
14667 matches!(result, MuxResult::Continue),
14668 "a quiesced write pass continues, got {result:?}"
14669 );
14670 }
14671
14672 // ── One pass, one prepare per stream ────────────────────────────────
14673 //
14674 // The four tests above pin the FLUSH half of a write pass. This one pins
14675 // the PREPARE half against the same refactor: `write_streams` runs the
14676 // gate at `if kawa.is_main_phase() || ...` — `kawa.prepare`, the
14677 // `*parts.window` debit, `flow_control.consume_send_window`,
14678 // `census.note_fired` and the `freshly_emitted_rsts` push — ONCE per
14679 // stream per pass, in `H2WritePhase::Prepare` and outside the round-again
14680 // `H2WritePhase::Flush` re-enters. A restructure that lifted those rounds
14681 // up beside the prepare would debit the send window twice for
14682 // one stream's single scheduling turn: a connection would believe it had
14683 // spent credit it never put on the wire, and would stall itself short of
14684 // the peer's real window.
14685 //
14686 // MEASURED, and it corrects the obvious way to write this test. A narrow
14687 // per-stream window does NOT expose a double prepare. With
14688 // `create_stream(ulid, 64)` against a 200-byte body and a two-round flush,
14689 // production and an edit that visits every stream twice in one pass give
14690 // the same `delta=64 vectored_calls=2 blocks_left=1 bout=73`. The send
14691 // window is SELF-LIMITING: `*parts.window =
14692 // parts.window.saturating_sub(consumed)` zeroes the stream's credit, the
14693 // second prepare computes `min(0, connection_window) == 0`, the converter's
14694 // `self.window > 0` gate returns the chunk untouched, and
14695 // `consumed = 0 - 0 = 0`. A non-incremental prepare always takes every byte
14696 // the window allows, so a second one has nothing left to take.
14697 //
14698 // INVISIBLE TO THOSE OBSERVABLES IS NOT HARMLESS, and the difference is
14699 // the reason this block is worth reading. Two things DO move on that
14700 // second visit, measured on the same fixture:
14701 //
14702 // - `converter.rs`'s flow-control-stall arm fires. It is the arm reached
14703 // BECAUSE the window is zero, and its `incr!(names::h2::FLOW_CONTROL_STALL)`
14704 // is unconditional once there — on `Position::Client` it also fires
14705 // `incr!(names::backend::FLOW_CONTROL_PAUSED)`. Measured with a `panic!`
14706 // planted on that `incr!`: the fixture does NOT reach it under
14707 // production and DOES under the double visit. Those two counters are
14708 // dashboard signals for a real RFC 9113 §6.9 condition, so a re-entrant
14709 // write loop would inflate them with stalls that never happened on the
14710 // wire — an operator would read backpressure into a connection that had
14711 // none.
14712 // - `context.debug.push(DebugEvent::S(..))` sits OUTSIDE the prepare
14713 // gate's closing brace, so it fires once per VISIT, not once per
14714 // prepare: measured `s_events` 1 -> 2 while `io_events` stays 2. Every
14715 // extra entry evicts a real one from the bounded `DebugHistory` ring.
14716 //
14717 // So the correct claim is narrow: the BYTES are idempotent, the
14718 // TELEMETRY is not.
14719 //
14720 // And the bytes are idempotent by FOUR independent mechanisms, only one of
14721 // which is the window — a future reader must not conclude the window is
14722 // the guard:
14723 //
14724 // 1. `consumed` debits `*parts.window` AND `flow_control`, which is the
14725 // self-limiting above;
14726 // 2. `H2Scheduler::note_fired` gates its bucket's round-robin lead on
14727 // `consumed > 0`, so a zero-consumption re-entry cannot advance the
14728 // cursor;
14729 // 3. the `rst_sent` `HashSet` in `H2StreamTable` dedups RST_STREAM.
14730 // This one is load-bearing rather than incidental:
14731 // `H2BlockConverter::initialize` pushes a RST_STREAM frame on EVERY
14732 // prepare of an errored kawa with no window gate whatsoever, so
14733 // without that set a second visit would put a duplicate RST on the
14734 // wire regardless of the send window;
14735 // 4. `std::mem::take(&mut parts.context.headers_response)` drains the
14736 // response-header edits, so a re-entry cannot apply them twice.
14737 //
14738 // The RFC 9218 §4 incremental yield is the one path that stops a prepare
14739 // with BOTH window and blocks remaining: `H2BlockConverter`'s DATA arm
14740 // returns `can_continue && !yield_after_data`, and with
14741 // `incremental_mode && incremental_peer_count > 1` it yields after a
14742 // single DATA frame while the window is still wide open. That is why this
14743 // test registers a same-urgency incremental PAIR rather than one stream,
14744 // and why the body must exceed `SETTINGS_MAX_FRAME_SIZE` (16 384) so one
14745 // frame cannot carry it.
14746
14747 /// A response body larger than the default 16 384-byte
14748 /// `SETTINGS_MAX_FRAME_SIZE`, so one DATA frame cannot carry it and the
14749 /// incremental yield leaves the remainder in `blocks`.
14750 static YIELDING_BODY: [u8; 40_000] = [b'x'; 40_000];
14751
14752 /// Payload bytes one yielded prepare converts: the default
14753 /// `SETTINGS_MAX_FRAME_SIZE`, which bounds the frame before the window does.
14754 const YIELDED_PAYLOAD: i32 = 16_384;
14755
14756 /// Bytes one yielded prepare puts in `kawa.out`: the DATA frame header
14757 /// plus its payload.
14758 const YIELDED_FRAME: usize = parser::FRAME_HEADER_SIZE + YIELDED_PAYLOAD as usize;
14759
14760 /// Two registered, same-urgency INCREMENTAL server streams, each carrying
14761 /// `YIELDING_BODY` as one un-prepared chunk and each granted a send window
14762 /// far wider than a single frame.
14763 ///
14764 /// The width is the point: a narrow window would make the prepare stop
14765 /// because it ran out of credit, and a second prepare would then be a
14766 /// no-op whether or not the code is correct. Here the prepare stops
14767 /// because it YIELDED, so credit survives it and a second one would spend
14768 /// more.
14769 fn incremental_pair_fixture(
14770 pool: &Rc<RefCell<Pool>>,
14771 script: &[(usize, SocketResult)],
14772 ) -> (
14773 H2Shell<BackpressuredTlsSocket>,
14774 Context<TestListener>,
14775 [GlobalStreamId; 2],
14776 std::net::TcpStream,
14777 ) {
14778 let (mut connection, peer) = connection_with_backpressure(pool, 0, 0, H2State::Header);
14779 connection.socket.vectored_script = script.iter().copied().collect();
14780 let mut context = test_context(pool);
14781 let mut gids = [0usize; 2];
14782 for (slot, stream_id) in [1u32, 3u32].into_iter().enumerate() {
14783 let gid = context
14784 .create_stream(Ulid::generate(), 1 << 16)
14785 .expect("test context must create a stream");
14786 gids[slot] = gid;
14787 connection
14788 .core
14789 .stream_table
14790 .register(stream_id, gid, connection.core.now);
14791 // Same urgency for both, so they share one bucket and the pass
14792 // census answers `incremental_peer_count == 2`. At 1 the
14793 // converter's solo-bucket guard suppresses the yield and the
14794 // prepare drains to the window, which is the configuration this
14795 // test exists to avoid.
14796 connection.core.scheduler.push_priority(
14797 stream_id,
14798 parser::PriorityPart::Rfc9218 {
14799 urgency: 3,
14800 incremental: true,
14801 },
14802 );
14803 let back = &mut context.streams[gid].back;
14804 back.parsing_phase = kawa::ParsingPhase::Body;
14805 back.blocks.push_back(kawa::Block::Chunk(kawa::Chunk {
14806 data: kawa::Store::Static(&YIELDING_BODY),
14807 }));
14808 }
14809 (connection, context, gids, peer)
14810 }
14811
14812 /// One write pass debits the connection send window once per stream, even
14813 /// when that stream's flush goes round twice.
14814 ///
14815 /// Stream 1's flush is scripted to take two rounds — `(20, WouldBlock)`
14816 /// then the rest — so the pass contains exactly the round-again shape the
14817 /// flush tests above pin, and this test says the prepare does NOT come
14818 /// along for the second round. Stream 3's single round is the control:
14819 /// both streams yield after one frame, and both must cost the same credit.
14820 ///
14821 /// WHAT THE EXISTING FIXTURE CANNOT SEE. `a_partial_write_reporting_would_block_continues_the_pass`
14822 /// drives the identical two-round flush and stays green under every edit
14823 /// below: its stream's parsing phase leaves the prepare gate shut by
14824 /// construction, so it never runs a prepare at all and no amount of
14825 /// re-preparing is representable in it. Its through-`writable()` twin
14826 /// cannot see it either, for the same reason.
14827 ///
14828 /// TO SEE THIS RED: in `begin_scheduler_pass`, insert
14829 /// `let order: Vec<StreamId> = order.iter().chain(order.iter()).copied().collect();`
14830 /// immediately before `pass.adopt_scheduler_pass(converter_pass, order, census);`,
14831 /// so each stream takes a second turn — prepare included — within one pass.
14832 /// Measured: this test fails on its own first assertion, `one write pass
14833 /// must debit the connection send window once per stream`, with
14834 /// `left: 65535, right: 32768`, and it is the ONLY failure in the suite.
14835 /// The four frames the mutated pass emits instead
14836 /// of two also move `vectored_calls` from 3 to 5 and each stream's `bout`
14837 /// from 16 393 to 32 786 / 32 785, so the window is not a privileged
14838 /// witness here — but it is the only one that names the SPENT CREDIT, and
14839 /// a window debited for bytes the wire never carried is the failure that
14840 /// stalls a connection. The same assertion is also the third failure under
14841 /// the pass-terminator edit documented on
14842 /// `a_partial_write_reporting_would_block_continues_the_pass_through_writable`,
14843 /// where it reads `left: 16384, right: 32768` because stream 1's stall
14844 /// ends the pass before stream 3 is prepared.
14845 ///
14846 /// This is NOT keyed on the partial write, and the name says so. The
14847 /// inversion makes the rounds visible — `H2WritePhase::Flush` is re-entered
14848 /// once per round — but the prepare is not in that phase to be repeated:
14849 /// it lives in `H2WritePhase::Prepare`, which a round-again never returns
14850 /// to. Forcing a second visit of the whole order is the smallest honest
14851 /// statement of the same bug, and it is what a restructure that folded the
14852 /// two phases together would actually produce.
14853 #[test]
14854 fn a_yielded_incremental_stream_is_prepared_once_per_pass() {
14855 let pool = make_pool_for_invariant_16();
14856 let (mut connection, mut context, gids, _peer) = incremental_pair_fixture(
14857 &pool,
14858 &[
14859 // Stream 1: a partial write that reports WouldBlock, then the
14860 // remainder. Stream 3: one round.
14861 (20, SocketResult::WouldBlock),
14862 (99_999, SocketResult::Continue),
14863 (99_999, SocketResult::Continue),
14864 ],
14865 );
14866 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
14867
14868 let window_before = connection.core.flow_control.window();
14869 assert_eq!(
14870 window_before, DEFAULT_INITIAL_WINDOW_SIZE as i32,
14871 "premise: the connection send window starts at the RFC 9113 \
14872 default, so the debit below is the pass's doing"
14873 );
14874 for gid in gids {
14875 assert!(
14876 context.streams[gid].back.is_main_phase(),
14877 "premise: the prepare gate must be OPEN for both streams, or \
14878 no window is spent at all"
14879 );
14880 }
14881
14882 let result = connection.writable(&mut context, EndpointClient(&mut router));
14883
14884 assert_eq!(
14885 window_before - connection.core.flow_control.window(),
14886 2 * YIELDED_PAYLOAD,
14887 "one write pass must debit the connection send window once per \
14888 stream: a second prepare would spend credit for bytes this pass \
14889 never put on the wire"
14890 );
14891 assert_eq!(
14892 connection.socket.vectored_calls, 3,
14893 "the pass must be two flush rounds for stream 1 and one for \
14894 stream 3 — without the two-round flush this test would not be \
14895 asserting anything about re-entry"
14896 );
14897 for gid in gids {
14898 assert_eq!(
14899 context.streams[gid].metrics.bout, YIELDED_FRAME,
14900 "each yielded prepare puts exactly one DATA frame on the wire"
14901 );
14902 assert!(
14903 !context.streams[gid].back.blocks.is_empty(),
14904 "premise: the yield must leave the rest of the body queued — \
14905 with empty blocks a second prepare could not spend anything \
14906 and this test could not fail"
14907 );
14908 }
14909 assert!(
14910 matches!(result, MuxResult::Continue),
14911 "a yielded write pass continues, got {result:?}"
14912 );
14913 }
14914
14915 /// The send window granted to the stream that makes this pass's progress.
14916 /// Narrower than `YIELDING_BODY`, so its prepare converts
14917 /// `PARTIAL_STREAM_WINDOW` payload bytes and leaves the rest in `blocks`.
14918 const PARTIAL_STREAM_WINDOW: u32 = 64;
14919
14920 /// Bytes that stream reaches the socket with: the DATA frame header plus
14921 /// its window-capped payload.
14922 const PARTIAL_FRAME: usize = parser::FRAME_HEADER_SIZE + PARTIAL_STREAM_WINDOW as usize;
14923
14924 /// A pass's byte total accumulates across streams: a later stream that
14925 /// writes nothing must not erase what an earlier one delivered.
14926 ///
14927 /// `pass.total_bytes_written.saturating_add(pass.stream_bytes)` in
14928 /// `H2WritePhase::Flush` is the whole of it, and nothing pinned it. `finalize_write` hands the
14929 /// total to `h2_close::finalize_action` as `made_progress`, and
14930 /// `made_progress && any_pending_back()` is LIFECYCLE §9 invariant 16 — the
14931 /// arm that RETAINS `Ready::WRITABLE` for a pass that moved bytes and left
14932 /// blocks behind. Drop the accumulation and a two-stream pass reports the
14933 /// LAST stream's count: stream 1 delivers a frame and keeps its remainder
14934 /// queued, stream 3 is eligible but window-blocked and writes nothing, the
14935 /// total reads 0, `expect_write` is parked on neither so `Parked` cannot
14936 /// mask it, `RetainPendingBack` is skipped and the pass falls through to
14937 /// `Quiesce`. `Quiesce` strips `Ready::WRITABLE`; edge-triggered epoll
14938 /// never re-fires; stream 1's body is stranded in `blocks` with nothing
14939 /// scheduled to drain it. That is a hung response, not a lost metric.
14940 ///
14941 /// The two streams differ only in their send window, which is what puts
14942 /// one on each side of the accumulation: 64 bytes buys stream 1 one
14943 /// window-capped DATA frame, and 0 buys stream 3 the converter's
14944 /// flow-control-stall arm, which returns the chunk untouched and leaves
14945 /// `kawa.out` empty so its flush never calls the socket at all.
14946 ///
14947 /// TO SEE THIS RED: in `poll_write_target`'s `H2WritePhase::Flush` arm,
14948 /// change the accumulation
14949 /// `pass.total_bytes_written = pass.total_bytes_written.saturating_add(pass.stream_bytes);`
14950 /// into `pass.total_bytes_written = pass.stream_bytes;` — the shape a
14951 /// rewrite that folds the resume and main flush paths into one per-stream
14952 /// counter naturally reaches for. Measured: this test fails on its own first
14953 /// assertion, `a pass that delivered bytes and left blocks queued must
14954 /// RETAIN WRITABLE interest`, with `left: Error | Hup, right: Writable |
14955 /// Error | Hup`, and it is the only failure in the suite. Measured without
14956 /// this test in the tree, that same edit leaves every other test green —
14957 /// which is what makes this one worth its lines.
14958 #[test]
14959 fn a_later_zero_byte_stream_does_not_erase_the_pass_progress() {
14960 let pool = make_pool_for_invariant_16();
14961 let (mut connection, _peer) = connection_with_backpressure(&pool, 0, 0, H2State::Header);
14962 // One entry: stream 1's frame. Stream 3 puts nothing in `kawa.out`, so
14963 // its flush loop never runs and never reaches the socket.
14964 connection.socket.vectored_script =
14965 [(99_999, SocketResult::Continue)].into_iter().collect();
14966 let mut context = test_context(&pool);
14967 let mut gids = [0usize; 2];
14968 for (slot, (stream_id, window)) in [(1u32, PARTIAL_STREAM_WINDOW), (3u32, 0)]
14969 .into_iter()
14970 .enumerate()
14971 {
14972 let gid = context
14973 .create_stream(Ulid::generate(), window)
14974 .expect("test context must create a stream");
14975 gids[slot] = gid;
14976 connection
14977 .core
14978 .stream_table
14979 .register(stream_id, gid, connection.core.now);
14980 let back = &mut context.streams[gid].back;
14981 back.parsing_phase = kawa::ParsingPhase::Body;
14982 back.blocks.push_back(kawa::Block::Chunk(kawa::Chunk {
14983 data: kawa::Store::Static(&YIELDING_BODY),
14984 }));
14985 }
14986 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
14987
14988 assert!(
14989 connection.core.readiness.interest.is_writable(),
14990 "premise: WRITABLE interest starts armed, so retaining it is not \
14991 vacuous, got {:?}",
14992 connection.core.readiness
14993 );
14994
14995 let result = connection.writable(&mut context, EndpointClient(&mut router));
14996
14997 assert_eq!(
14998 connection.core.readiness.interest,
14999 Ready::WRITABLE | Ready::HUP | Ready::ERROR,
15000 "a pass that delivered bytes and left blocks queued must RETAIN \
15001 WRITABLE interest (LIFECYCLE §9 invariant 16): stripping it \
15002 strands the remainder, because edge-triggered epoll will not \
15003 re-fire on its own"
15004 );
15005 assert_eq!(
15006 context.streams[gids[0]].metrics.bout, PARTIAL_FRAME,
15007 "premise: the FIRST stream really did deliver a frame — the \
15008 accumulation has nothing to preserve otherwise"
15009 );
15010 assert_eq!(
15011 context.streams[gids[1]].metrics.bout, 0,
15012 "premise: the SECOND stream really did write nothing, so it is the \
15013 stream whose count would overwrite the total"
15014 );
15015 assert_eq!(
15016 connection.socket.vectored_calls, 1,
15017 "only the first stream reaches the socket: a zero window leaves \
15018 `kawa.out` empty and its flush loop never runs"
15019 );
15020 for gid in gids {
15021 assert!(
15022 !context.streams[gid].back.blocks.is_empty(),
15023 "premise: both streams must leave blocks queued, or \
15024 any_pending_back answers false and RetainPendingBack is \
15025 unreachable whatever the byte total says"
15026 );
15027 }
15028 assert_eq!(
15029 connection.core.stream_table.expect_write(),
15030 None,
15031 "premise: nothing is parked, so `Parked` cannot decide this pass \
15032 ahead of the byte total"
15033 );
15034 assert!(
15035 matches!(result, MuxResult::Continue),
15036 "a retained write pass continues, got {result:?}"
15037 );
15038 }
15039
15040 // ── force_disconnect's server arm with records pending ──────────────
15041 //
15042 // `h2_close::force_disconnect_action` is exhaustively tabled over its two
15043 // booleans in that module. Nothing called `ConnectionH2::force_disconnect`
15044 // itself, so the CALLER half — the `H2Shell::tls_wants_write` query
15045 // that feeds the table and the wiring of `ReArmAndContinue` to interest,
15046 // flush and return value — had no coverage. #1454 asks for this site by
15047 // name.
15048 //
15049 // Shape targeted: a SINGLE query, not a triple. `force_disconnect` reads
15050 // the answer once into `tls_wants_write`, above its `match` and therefore
15051 // for both position arms, feeds the table and reports the same value from
15052 // every debug line; there is no `H2Shell::flush_tls_records` here and
15053 // so no second question to reconstruct.
15054
15055 /// A server connection whose TLS records are still buffered re-arms
15056 /// instead of closing the session.
15057 ///
15058 /// `MuxResult::CloseSession` triggers `shutdown(Write)`, which sends FIN
15059 /// and destroys whatever rustls is still holding — the client reads a
15060 /// truncated response. So the live-peer, records-pending row of
15061 /// `force_disconnect_action` must keep WRITABLE and let the writable path
15062 /// flush.
15063 ///
15064 /// TO SEE THIS RED: in `force_disconnect`'s `Position::Server` arm, pass
15065 /// `false` in place of `tls_wants_write` to `force_disconnect_action`.
15066 /// Measured: this test fails on its own first assertion, `a server still
15067 /// holding TLS records must not close: CloseSession sends FIN and destroys
15068 /// them, got CloseSession`, while `h2_close`'s own
15069 /// `force_disconnect_arm_is_exhaustive` stays green — the table is not the
15070 /// caller, which is why this test exists. Passing `true` instead would NOT
15071 /// redden this test; catching that constant ON THIS DIRECT PATH is what
15072 /// the sibling below is for — it is not the suite's only witness against
15073 /// `true`, and its own doc says which other test is.
15074 ///
15075 /// The interest assertion has its own recipe: delete
15076 /// `self.readiness.interest = Ready::WRITABLE | Ready::HUP | Ready::ERROR;`
15077 /// from the same arm. Measured: `the re-arm must restore WRITABLE interest
15078 /// so the writable path is called again to flush`, `left: Hup, right:
15079 /// Writable | Error | Hup`. That recipe only bites because the test
15080 /// withdraws the bit first — asserting it against the fixture's own
15081 /// constructor argument passed under this very mutation.
15082 #[test]
15083 fn force_disconnect_re_arms_while_tls_records_are_pending() {
15084 let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
15085 // A kernel that accepts nothing, so the records outlive any flush.
15086 let (mut connection, _peer) = connection_with_backpressure(&pool, 2, 0, H2State::GoAway);
15087
15088 assert!(
15089 connection.socket.socket_wants_write(),
15090 "premise: this harness must report buffered TLS records"
15091 );
15092 assert!(
15093 !connection.core.peer_gone_after_final_goaway(),
15094 "premise: the peer must still be live, or the table short-circuits \
15095 to CloseSession on the first boolean and the record count is never \
15096 read"
15097 );
15098 // Withdraw WRITABLE interest first. `connection_with_backpressure`
15099 // hands `WRITABLE | HUP | ERROR` to `ConnectionH2::new` as the
15100 // connection's INTEREST, so asserting on it untouched would be reading
15101 // this fixture's own argument back and would hold even if the re-arm
15102 // arm never assigned it. Starting from `HUP` alone makes the
15103 // assignment the only thing that can restore the bit.
15104 connection.core.readiness.interest = Ready::HUP;
15105 assert!(
15106 !connection.core.readiness.event.is_writable(),
15107 "premise: the WRITABLE event must start clear, so the signal below \
15108 is the only thing that can set it, got {:?}",
15109 connection.core.readiness
15110 );
15111
15112 let result = connection.force_disconnect();
15113
15114 assert!(
15115 matches!(result, MuxResult::Continue),
15116 "a server still holding TLS records must not close: CloseSession \
15117 sends FIN and destroys them, got {result:?}"
15118 );
15119 assert_eq!(
15120 connection.core.readiness.interest,
15121 Ready::WRITABLE | Ready::HUP | Ready::ERROR,
15122 "the re-arm must restore WRITABLE interest so the writable path is \
15123 called again to flush, got {:?}",
15124 connection.core.readiness
15125 );
15126 assert!(
15127 connection.core.readiness.event.is_writable(),
15128 "the re-arm must signal the edge-triggered WRITABLE event, or the \
15129 event loop never revisits this connection, got {:?}",
15130 connection.core.readiness
15131 );
15132 }
15133
15134 /// The same call with nothing buffered closes the session.
15135 ///
15136 /// The other row of the table. It does NOT rest on being the only thing
15137 /// that catches a hard-wired `true`: the suite already caught that
15138 /// constant without it. Measured on that mutation, TWO tests fail — this
15139 /// test AND
15140 /// `a_flush_that_succeeds_closes_within_one_writable_call` above, which
15141 /// predates it and reaches `force_disconnect` through `writable()`'s
15142 /// GoAway fall-through, failing with `a drained flush must reach the
15143 /// disconnect in the SAME writable call, not defer it to another tick:
15144 /// got Continue`.
15145 ///
15146 /// What this test adds is the PATH, not the count: it calls
15147 /// `ConnectionH2::force_disconnect` directly, so a red here names the
15148 /// query under test, where the fall-through route arrives through a write
15149 /// pass and a state machine that can each shift for reasons of their own.
15150 ///
15151 /// TO SEE THIS RED: pass `true` in place of `tls_wants_write` to
15152 /// `force_disconnect_action` in `force_disconnect`'s `Position::Server`
15153 /// arm. Measured: this test fails with `a server holding nothing must
15154 /// close rather than wait for a flush that has nothing to flush, got
15155 /// Continue`, while the sibling above stays green. The mirror constant
15156 /// `false` is measured at exactly ONE failure, and that failure is
15157 /// the sibling above — so neither constant can replace the query, and the
15158 /// sibling is the suite's only witness against `false`.
15159 #[test]
15160 fn force_disconnect_closes_when_no_tls_records_are_pending() {
15161 let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
15162 // Same fixture, zero records: the query answers `false`.
15163 let (mut connection, _peer) = connection_with_backpressure(&pool, 0, 0, H2State::GoAway);
15164
15165 assert!(
15166 !connection.socket.socket_wants_write(),
15167 "premise: nothing buffered, so the table reads its second boolean \
15168 as false"
15169 );
15170 assert!(
15171 !connection.core.peer_gone_after_final_goaway(),
15172 "premise: the peer must still be live, so the close comes from the \
15173 record count and not from the first boolean"
15174 );
15175
15176 let result = connection.force_disconnect();
15177
15178 assert!(
15179 matches!(result, MuxResult::CloseSession),
15180 "a server holding nothing must close rather than wait for a flush \
15181 that has nothing to flush, got {result:?}"
15182 );
15183 }
15184
15185 /// A force-disconnect raised from the READ path still re-arms, because the
15186 /// shell settles it before the pass returns.
15187 ///
15188 /// The sibling above calls `H2Shell::force_disconnect` directly, where the
15189 /// raise and the settlement are two statements of one function. This one
15190 /// enters through `H2Shell::readable`, which is the shape that made the
15191 /// inversion necessary: `ConnectionH2::handle_read` force-disconnects at
15192 /// seven sites, its `pub` signature returns `MuxResult` and cannot carry an
15193 /// `H2ForceDisconnectTarget`, so the core parks one and answers
15194 /// provisionally. Nothing the core does afterwards writes `readiness`; only
15195 /// `H2Shell::settled` does, and only if the shell remembers to call it.
15196 ///
15197 /// `Mux` reads `Readiness::filter_interest` between entry points to decide
15198 /// whether a connection gets a `writable` at all, so a settlement deferred
15199 /// to the next entry point is not the same as one performed here: the
15200 /// connection would be left with records in rustls and no WRITABLE
15201 /// interest to come back on.
15202 ///
15203 /// TO SEE THIS RED: in `H2Shell::readable`, return `result` instead of
15204 /// `self.settled(result)`. Measured: this test fails on its interest
15205 /// assertion, `a force-disconnect raised inside handle_read must still
15206 /// re-arm WRITABLE interest before the read pass returns`, `left: Hup`,
15207 /// `right: Writable | Error | Hup`, while
15208 /// `force_disconnect_re_arms_while_tls_records_are_pending` stays green —
15209 /// the direct call is not the read path, which is why both exist.
15210 #[test]
15211 fn a_force_disconnect_raised_inside_handle_read_is_settled_before_the_pass_returns() {
15212 use std::io::Write;
15213
15214 let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
15215 // A kernel that accepts nothing, so the records outlive any flush and
15216 // `force_disconnect_action` takes its `ReArmAndContinue` row.
15217 let (mut connection, mut peer) =
15218 connection_with_backpressure(&pool, 2, 0, H2State::ClientPreface);
15219 let mut context = test_context(&pool);
15220 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
15221
15222 assert!(
15223 connection.socket.socket_wants_write(),
15224 "premise: this harness must report buffered TLS records, or the \
15225 settlement takes the closing row and writes no interest at all"
15226 );
15227 assert!(
15228 !connection.core.peer_gone_after_final_goaway(),
15229 "premise: the peer must still be live, or the decision \
15230 short-circuits to CloseSession on its first boolean and never \
15231 reads the record count"
15232 );
15233
15234 // A preface whose very first octet is wrong: `handle_read`'s early
15235 // guard refuses it and reaches `force_disconnect` without the
15236 // connection ever leaving `H2State::ClientPreface` on its own.
15237 let mut wire = Vec::with_capacity(CLIENT_PREFACE_SIZE);
15238 wire.extend_from_slice(b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n");
15239 wire.extend_from_slice(&[0, 0, 0, 4, 0, 0, 0, 0, 0]);
15240 wire[0] ^= 0xff;
15241 peer.write_all(&wire).expect("loopback write must complete");
15242 peer.flush().expect("loopback flush must complete");
15243
15244 // Withdraw WRITABLE interest first. The fixture hands
15245 // `WRITABLE | HUP | ERROR` to the constructor, so asserting on it
15246 // untouched would read the fixture's own argument back and would hold
15247 // even if nothing settled. Starting from `HUP` alone makes the
15248 // settlement the only thing that can restore the bit.
15249 connection.core.readiness.interest = Ready::HUP;
15250 assert!(
15251 !connection.core.readiness.event.is_writable(),
15252 "premise: the WRITABLE event must start clear, so the signal below \
15253 is the only thing that can set it, got {:?}",
15254 connection.core.readiness
15255 );
15256
15257 let mut result = MuxResult::Continue;
15258 for _ in 0..64 {
15259 result = connection.readable(&mut context, EndpointClient(&mut router));
15260 if matches!(connection.core.state, H2State::Error) {
15261 break;
15262 }
15263 std::thread::yield_now();
15264 }
15265
15266 assert!(
15267 matches!(connection.core.state, H2State::Error),
15268 "premise: the read pass must have reached force_disconnect, which \
15269 assigns H2State::Error, got {:?}",
15270 connection.core.state
15271 );
15272 assert!(
15273 matches!(result, MuxResult::Continue),
15274 "a server still holding TLS records must not close: CloseSession \
15275 sends FIN and destroys them, got {result:?}"
15276 );
15277 assert_eq!(
15278 connection.core.readiness.interest,
15279 Ready::WRITABLE | Ready::HUP | Ready::ERROR,
15280 "a force-disconnect raised inside handle_read must still re-arm \
15281 WRITABLE interest before the read pass returns, got {:?}",
15282 connection.core.readiness
15283 );
15284 assert!(
15285 connection.core.readiness.event.is_writable(),
15286 "the settlement must signal the edge-triggered WRITABLE event, or \
15287 the event loop never revisits this connection, got {:?}",
15288 connection.core.readiness
15289 );
15290 assert!(
15291 connection.core.poll_force_disconnect().is_none(),
15292 "the settlement must have TAKEN the parked target, so a second \
15293 settlement cannot run"
15294 );
15295 }
15296
15297 /// Build a bare server-side `ConnectionH2` for tests that only exercise
15298 /// connection-level bookkeeping. The socket is never read or written.
15299 ///
15300 /// Returns the accepted peer alongside it; keep it alive for the duration
15301 /// of the test so the socket stays connected and reads answer `WouldBlock`
15302 /// instead of `ECONNREFUSED`.
15303 fn test_h2_connection(
15304 pool: &Rc<RefCell<Pool>>,
15305 graceful_shutdown_deadline: Option<Duration>,
15306 ) -> (H2Shell<mio::net::TcpStream>, std::net::TcpStream) {
15307 let (socket, peer) = connected_socket();
15308 let connection = H2Shell::new(
15309 Ulid::generate(),
15310 socket,
15311 Position::Server,
15312 &mut PoolBufferSource::new(Rc::downgrade(pool)),
15313 H2FloodConfig::default(),
15314 H2ConnectionConfig::default(),
15315 Duration::from_secs(30),
15316 graceful_shutdown_deadline,
15317 Duration::from_secs(30),
15318 Some((H2StreamId::Zero, CLIENT_PREFACE_SIZE)),
15319 Ready::READABLE | Ready::HUP | Ready::ERROR,
15320 )
15321 .expect("a pool with free buffers must yield an H2 connection");
15322 (connection, peer)
15323 }
15324
15325 /// `snapshot_rtts` samples the frontend once per connection, when the
15326 /// first stream of the connection is logged, and hands every later stream
15327 /// the same value, in the same pass or in a later one (issues #1339 Q11
15328 /// and #1590).
15329 ///
15330 /// The first two calls stand for two streams recycling inside one pass:
15331 /// all five production callers of this method sit on a recycle or reset
15332 /// path, and more than one of them can run in a single `write_streams`
15333 /// sweep. The third, through a fresh `ShellEndpoint`, stands for a stream
15334 /// logged in a later pass. The counter is
15335 /// [`super::super::tcp_info_reads`], bumped by every
15336 /// `getsockopt(TCP_INFO)` the mux issues.
15337 ///
15338 /// The premise is that the sample is `Some`: the connection sits on a live
15339 /// loopback socket, so `TCP_INFO` answers. Without it a `local_rtt` that
15340 /// had lost the socket would pass the equality assertions while reporting
15341 /// nothing at all.
15342 ///
15343 /// TO SEE THIS RED: in `memoized_rtt` (`lib/src/protocol/mux/mod.rs`),
15344 /// delete the early `return sample;` of the memo hit. Every call then
15345 /// reads the socket and the second assertion fails with ``a second stream
15346 /// finishing in the SAME pass must reuse the connection sample, not read
15347 /// TCP_INFO again``, `left: 2`, `right: 1`.
15348 #[test]
15349 fn snapshot_rtts_samples_the_frontend_once_per_connection() {
15350 let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
15351 let (connection, _peer) = test_h2_connection(&pool, None);
15352 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
15353 let mut inner = EndpointClient(&mut router);
15354
15355 let before = super::super::tcp_info_reads();
15356 let endpoint = ShellEndpoint::new(&mut inner, &connection.socket, &connection.local_rtt);
15357 let first = connection
15358 .core
15359 .snapshot_rtts(&endpoint, Some(mio::Token(1)));
15360 assert!(
15361 first.0.is_some(),
15362 "premise: TCP_INFO must answer on a live loopback frontend"
15363 );
15364 let second = connection
15365 .core
15366 .snapshot_rtts(&endpoint, Some(mio::Token(2)));
15367 assert_eq!(
15368 super::super::tcp_info_reads() - before,
15369 1,
15370 "a second stream finishing in the SAME pass must reuse the \
15371 connection sample, not read TCP_INFO again"
15372 );
15373 assert_eq!(
15374 second.0, first.0,
15375 "every stream finishing in one pass reports one client_rtt — the \
15376 identity #1339 Q11 traded the per-recycle syscall for"
15377 );
15378 assert_eq!(
15379 (first.1, second.1),
15380 (None, None),
15381 "the router holds no backend, so server_rtt stays None and costs \
15382 no read"
15383 );
15384
15385 let endpoint = ShellEndpoint::new(&mut inner, &connection.socket, &connection.local_rtt);
15386 let next_pass = connection.core.snapshot_rtts(&endpoint, None);
15387 assert_eq!(
15388 super::super::tcp_info_reads() - before,
15389 1,
15390 "a stream logged in a later pass must reuse the connection's one \
15391 sample, not read TCP_INFO again"
15392 );
15393 assert_eq!(
15394 next_pass.0, first.0,
15395 "every stream of one connection reports that connection's one \
15396 client_rtt"
15397 );
15398 }
15399
15400 /// A pass that logs no stream reads no `TCP_INFO`, and a backend H2
15401 /// connection never reads one for the access log (issue #1590).
15402 ///
15403 /// TO SEE THIS RED: make `ShellEndpoint::new` sample eagerly — call
15404 /// `super::sample_rtt(socket.socket_ref())` and store it in `sample` —
15405 /// which is the shape `Mux::refresh_client_rtt` had. The first assertion
15406 /// fails with ``a pass that logs no stream must not read TCP_INFO``,
15407 /// `left: 1`, `right: 0`.
15408 #[test]
15409 fn a_pass_that_logs_nothing_reads_no_tcp_info() {
15410 let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
15411 let (mut connection, _peer) = test_h2_connection(&pool, None);
15412 let mut context = test_context(&pool);
15413 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
15414
15415 let before = super::super::tcp_info_reads();
15416 connection.cancel_timed_out_streams(&mut context, &mut EndpointClient(&mut router));
15417 assert_eq!(
15418 super::super::tcp_info_reads() - before,
15419 0,
15420 "a pass that logs no stream must not read TCP_INFO"
15421 );
15422 assert_eq!(connection.local_rtt.get(), None);
15423
15424 connection.core.position = Position::Client(
15425 "rtt-cluster".into(),
15426 super::super::BackendId {
15427 slot: super::super::BackendSlot(0),
15428 backend_id: Rc::from("rtt-backend"),
15429 address: "127.0.0.1:1".parse().expect("a literal socket address"),
15430 metric_incarnation: Default::default(),
15431 },
15432 BackendStatus::Connected,
15433 );
15434 let mut inner = EndpointClient(&mut router);
15435 let endpoint = ShellEndpoint::new(&mut inner, &connection.socket, &connection.local_rtt);
15436 assert_eq!(
15437 connection
15438 .core
15439 .snapshot_rtts(&endpoint, Some(mio::Token(1))),
15440 (None, None),
15441 "a backend H2 connection must not report RTTs onto the shared stream"
15442 );
15443 assert_eq!(
15444 super::super::tcp_info_reads() - before,
15445 0,
15446 "a backend H2 connection must not read TCP_INFO for the access log"
15447 );
15448 }
15449
15450 /// Two streams reaped in one pass: two access-log lines, one frontend
15451 /// `TCP_INFO` read between them, and the same `client_rtt` on both lines.
15452 ///
15453 /// Drives the real log path — `H2Shell::cancel_timed_out_streams` through
15454 /// `ShellEndpoint`, `snapshot_rtts` and `Stream::generate_access_log` —
15455 /// and reads the lines the logger wrote, so it pins what an operator sees:
15456 /// the `client_rtt` cell is present (not `-`) and coherent across the
15457 /// pass, and `server_rtt` is `-` for streams that never reached a backend.
15458 ///
15459 /// TO SEE THIS RED: in `ConnectionH2::snapshot_rtts`, return `None` in
15460 /// place of `endpoint.local_rtt()`. Both lines then carry `-` in the
15461 /// `client_rtt` cell and the presence assertion fails.
15462 #[test]
15463 fn streams_reaped_in_one_pass_log_one_present_client_rtt() {
15464 let reads = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
15465 let reads_in_pass = reads.clone();
15466 let output = crate::capture_test_logs(move || {
15467 let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 8, 16384)));
15468 let (mut connection, _peer) = test_h2_connection(&pool, None);
15469 let mut context = test_context(&pool);
15470 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
15471
15472 let armed_at = connection.core.now;
15473 for stream_id in [1, 3] {
15474 let gid = context
15475 .create_stream(Ulid::generate(), 1 << 16)
15476 .expect("test context must create a stream");
15477 connection
15478 .core
15479 .stream_table
15480 .register(stream_id, gid, armed_at);
15481 }
15482 context.now = armed_at + connection.core.stream_idle_timeout + Duration::from_millis(1);
15483
15484 let before = super::super::tcp_info_reads();
15485 connection.cancel_timed_out_streams(&mut context, &mut EndpointClient(&mut router));
15486 reads_in_pass.store(
15487 super::super::tcp_info_reads() - before,
15488 std::sync::atomic::Ordering::Relaxed,
15489 );
15490 });
15491
15492 let cells: Vec<(&str, &str)> = output
15493 .lines()
15494 .filter_map(|line| {
15495 line.split_whitespace().find_map(|token| {
15496 let cells: Vec<&str> = token.split('/').collect();
15497 (cells.len() == 5).then(|| (cells[3], cells[4]))
15498 })
15499 })
15500 .collect();
15501 assert_eq!(
15502 cells.len(),
15503 2,
15504 "two reaped streams must write two access-log lines, got: {output}"
15505 );
15506 for (client_rtt, server_rtt) in &cells {
15507 assert_ne!(
15508 *client_rtt, "-",
15509 "the access log's client_rtt must be present, got: {output}"
15510 );
15511 assert_eq!(
15512 *server_rtt, "-",
15513 "a stream that never reached a backend has no server_rtt, got: {output}"
15514 );
15515 }
15516 assert_eq!(
15517 cells[0].0, cells[1].0,
15518 "streams logged in one pass share one client_rtt, got: {output}"
15519 );
15520 assert_eq!(
15521 reads.load(std::sync::atomic::Ordering::Relaxed),
15522 1,
15523 "two streams logged in one pass must cost one frontend TCP_INFO \
15524 read and no backend read"
15525 );
15526 }
15527
15528 /// The graceful-shutdown budget is armed from the `now` handed to
15529 /// `graceful_goaway` and evaluated against `ConnectionH2::now` — not
15530 /// against the real clock at either end.
15531 ///
15532 /// `armed_at` is deliberately 100 s in the future relative to the
15533 /// connection's construction snapshot. That gap is what makes the test
15534 /// discriminating: a `graceful_goaway` that armed `started_at` from
15535 /// `Instant::now()` would put it ~100 s in the PAST relative to
15536 /// `armed_at`, and the "not yet elapsed" assertion would trip.
15537 ///
15538 /// To SEE THIS RED: restore either half of the old behaviour.
15539 /// (a) `self.started_at = Some(Instant::now());` in
15540 /// `h2_drain::H2DrainState::begin_graceful_drain` — the `assert_eq!`
15541 /// on `drain.__test_started_at()` fails: ``assertion `left == right`
15542 /// failed: graceful_goaway must arm the budget from its `now`
15543 /// parameter / left: Some(Instant { tv_sec: 107343, .. }) / right:
15544 /// Some(Instant { tv_sec: 107443, .. })`` — the two differing by
15545 /// exactly the 100 s offset above.
15546 /// (b) `started_at.elapsed() >= deadline` in
15547 /// `h2_drain::H2DrainState::deadline_elapsed` — the LAST assertion
15548 /// fails with `advancing only the connection snapshot past the
15549 /// budget must force close`, because no real time passes in this
15550 /// test, so the forced-close budget never expires however far the
15551 /// connection's clock is moved.
15552 #[test]
15553 fn graceful_shutdown_deadline_is_evaluated_against_the_connection_snapshot() {
15554 let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
15555 let deadline = Duration::from_secs(5);
15556 let (mut connection, _peer) = test_h2_connection(&pool, Some(deadline));
15557
15558 // Nothing armed yet: `started_at` is None, so the budget cannot expire.
15559 assert!(
15560 !connection.core.graceful_shutdown_deadline_elapsed(),
15561 "an unarmed drain must never report its budget as elapsed"
15562 );
15563
15564 let armed_at = connection.core.now + Duration::from_secs(100);
15565 connection.graceful_goaway(armed_at);
15566 assert_eq!(
15567 connection.core.drain.__test_started_at(),
15568 Some(armed_at),
15569 "graceful_goaway must arm the budget from its `now` parameter"
15570 );
15571
15572 // One millisecond short of the budget, measured on the connection's
15573 // clock. Real elapsed time here is microseconds.
15574 connection.core.now = armed_at + deadline - Duration::from_millis(1);
15575 assert!(
15576 !connection.core.graceful_shutdown_deadline_elapsed(),
15577 "the budget must not expire before `started_at + deadline` on the \
15578 connection's own clock"
15579 );
15580
15581 connection.core.now = armed_at + deadline;
15582 assert!(
15583 connection.core.graceful_shutdown_deadline_elapsed(),
15584 "advancing only the connection snapshot past the budget must force close"
15585 );
15586 }
15587
15588 /// The RFC 9113 §5.1.2 back-pressure window is driven by
15589 /// `ConnectionH2::now`, so a refusal burst is weighed against the window
15590 /// that was open when the pass started — it cannot decay mid-pass.
15591 ///
15592 /// To SEE THIS RED: restore
15593 /// `if self.refuse_window_start.elapsed() >= BACKPRESSURE_WINDOW_DURATION`
15594 /// in `record_refusal_for_backpressure`. The window then never rolls over
15595 /// (no real time passes) and the 50th refusal lands in the same window as
15596 /// the first 49, so the `refuse_count_window` assertion fails first:
15597 /// ``assertion `left == right` failed: a refusal in a new window must
15598 /// restart the count, not top up the old one / left: 1000 / right: 1``. The
15599 /// two assertions after it would also fail — `mcs_backpressure_applied`
15600 /// becomes true and `settings_max_concurrent_streams` is halved off a burst
15601 /// that actually spanned two windows.
15602 #[test]
15603 fn backpressure_window_rolls_over_on_the_connection_snapshot() {
15604 let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
15605 let (mut connection, _peer) = test_h2_connection(&pool, None);
15606 let advertised = connection
15607 .core
15608 .local_settings
15609 .settings_max_concurrent_streams;
15610
15611 // One short of the burst threshold, all inside the first window.
15612 for _ in 0..BACKPRESSURE_REFUSAL_THRESHOLD - 1 {
15613 connection.core.record_refusal_for_backpressure();
15614 }
15615 assert_eq!(
15616 connection.core.refuse_count_window,
15617 BACKPRESSURE_REFUSAL_THRESHOLD - 1
15618 );
15619 assert!(
15620 !connection.core.mcs_backpressure_applied,
15621 "back-pressure must not apply below the refusal threshold"
15622 );
15623
15624 // Cross into the next window on the connection's clock alone.
15625 connection.core.now += BACKPRESSURE_WINDOW_DURATION;
15626 connection.core.record_refusal_for_backpressure();
15627
15628 assert_eq!(
15629 connection.core.refuse_count_window, 1,
15630 "a refusal in a new window must restart the count, not top up the old one"
15631 );
15632 assert!(
15633 !connection.core.mcs_backpressure_applied,
15634 "two refusals spread across two windows are not a burst"
15635 );
15636 assert_eq!(
15637 connection
15638 .core
15639 .local_settings
15640 .settings_max_concurrent_streams,
15641 advertised,
15642 "MAX_CONCURRENT_STREAMS must be untouched when no burst occurred"
15643 );
15644 }
15645
15646 /// The RFC 9113 §6.5 SETTINGS-ACK deadline is evaluated against
15647 /// `ConnectionH2::now`. `flush_pending_control_frames` is the `writable`
15648 /// half of the pair; the `poll_read_target` half at `h2.rs` shares the predicate.
15649 ///
15650 /// To SEE THIS RED: restore `sent_at.elapsed() >= SETTINGS_ACK_TIMEOUT` in
15651 /// `flush_pending_control_frames`. No real time passes in this test, so the
15652 /// deadline never fires however far the connection's clock is advanced, and
15653 /// the third assertion fails with `advancing only the connection snapshot
15654 /// to the deadline must emit GOAWAY`.
15655 #[test]
15656 fn settings_ack_deadline_is_evaluated_against_the_connection_snapshot() {
15657 let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
15658 let (mut connection, _peer) = test_h2_connection(&pool, None);
15659
15660 let sent_at = connection.core.now;
15661 connection.core.settings_sent_at = Some(sent_at);
15662
15663 // One millisecond short of the budget on the connection's clock.
15664 connection.core.now = sent_at + SETTINGS_ACK_TIMEOUT - Duration::from_millis(1);
15665 assert!(
15666 matches!(
15667 connection.core.flush_pending_control_frames(),
15668 H2ControlFlushTarget::Proceed
15669 ),
15670 "the SETTINGS-ACK deadline must not fire before SETTINGS_ACK_TIMEOUT \
15671 on the connection's own clock"
15672 );
15673 assert_eq!(
15674 connection.core.settings_sent_at,
15675 Some(sent_at),
15676 "a deadline that has not fired must leave the ACK timer armed"
15677 );
15678
15679 // At the budget: GOAWAY(SETTINGS_TIMEOUT).
15680 connection.core.now = sent_at + SETTINGS_ACK_TIMEOUT;
15681 assert!(
15682 matches!(
15683 connection.core.flush_pending_control_frames(),
15684 H2ControlFlushTarget::Done(_)
15685 ),
15686 "advancing only the connection snapshot to the deadline must emit GOAWAY"
15687 );
15688 assert_eq!(
15689 connection.core.settings_sent_at, None,
15690 "goaway must disarm the ACK timer so the timeout cannot re-fire"
15691 );
15692 }
15693
15694 /// The per-stream liveness guard reaps against `ConnectionH2::now`, and
15695 /// `cancel_timed_out_streams` adopts `context.now` on entry — this pins
15696 /// both the entry-point mirror and the deadline arithmetic in
15697 /// `H2StreamTable::collect_timed_out`.
15698 ///
15699 /// To SEE THIS RED: restore `let now = Instant::now();` in
15700 /// `cancel_timed_out_streams` (in place of `let now = self.now;`). The
15701 /// injected instant is then ignored, no stream is ever old enough to reap
15702 /// because no real time passes, and the reap assertion fails with
15703 /// `advancing only the mux snapshot past the idle deadline must reap the
15704 /// stream`. Deleting the `self.now = context.now;` entry-point assignment
15705 /// turns the same assertion red, with the same message, for the same reason.
15706 #[test]
15707 fn per_stream_liveness_reaping_is_evaluated_against_the_connection_snapshot() {
15708 let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
15709 let (mut connection, _peer) = test_h2_connection(&pool, None);
15710 let mut context = test_context(&pool);
15711 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
15712
15713 let idle_timeout = connection.core.stream_idle_timeout;
15714 let armed_at = connection.core.now;
15715 let gid = context
15716 .create_stream(Ulid::generate(), 1 << 16)
15717 .expect("test context must create a stream");
15718 connection.core.stream_table.register(1, gid, armed_at);
15719
15720 // Exactly at the deadline: the predicate is a strict `>`, so the stream
15721 // survives.
15722 context.now = armed_at + idle_timeout;
15723 connection.cancel_timed_out_streams(&mut context, &mut EndpointClient(&mut router));
15724 assert!(
15725 connection.core.control_tx.pending().is_empty(),
15726 "a stream exactly at its idle deadline must not be reaped"
15727 );
15728
15729 // One millisecond past it: reaped with RST_STREAM(CANCEL).
15730 context.now = armed_at + idle_timeout + Duration::from_millis(1);
15731 connection.cancel_timed_out_streams(&mut context, &mut EndpointClient(&mut router));
15732 assert!(
15733 !connection.core.control_tx.pending().is_empty(),
15734 "advancing only the mux snapshot past the idle deadline must reap the stream"
15735 );
15736 assert_eq!(
15737 connection.core.control_tx.pending()[0],
15738 (1, H2Error::Cancel),
15739 "the reaper must queue RST_STREAM(CANCEL) for the timed-out stream"
15740 );
15741 }
15742
15743 /// A mass idle-timeout reap is the only path that queues more than one
15744 /// RST_STREAM between two `flush_pending_control_frames` passes:
15745 /// `cancel_timed_out_streams` walks the whole timed-out set and calls
15746 /// `enqueue_rst` for every entry inside ONE pass. The cap therefore has to
15747 /// live at the insert, not at the drain — a drain-side check bounds what
15748 /// is *written*, and says nothing about how large the queue got before
15749 /// anyone looked. See sozu-proxy/sozu#1413.
15750 ///
15751 /// `h2_control_tx::pending_rst_bound` keeps the bound above four resets
15752 /// per advertised concurrent stream, so a real sweep cannot reach it on
15753 /// its own; the queue still holds what every caller queued since the last
15754 /// successful drain. This test registers more wire ids than the bound on
15755 /// the stream table directly, exactly as its sibling
15756 /// `per_stream_liveness_reaping_is_evaluated_against_the_connection_snapshot`
15757 /// does, because the accept path is not what is under test here.
15758 ///
15759 /// `check_invariants` is asserted explicitly because the reaper is also
15760 /// reachable from `Mux::timeout` against a fully silent peer — the very
15761 /// scenario that produces a mass reap — where `handle_frame`, the only
15762 /// production caller of `check_invariants`, never runs at all. The reaper
15763 /// is not the only insert path outside that caller: the
15764 /// DATA-on-closed-stream reset sits in `handle_header_state`, which
15765 /// `handle_read` returns into without ever reaching `handle_frame`. That one
15766 /// is rate-limited by `record_glitch` + `check_flood_or_return!` and
15767 /// cannot by itself fill the queue — see `ConnectionH2::enqueue_rst`.
15768 ///
15769 /// To SEE THIS RED: delete the `pending_before >= self.max_pending` guard
15770 /// at the top of `H2ControlTx::enqueue_rst` (`h2_control_tx.rs`), then run
15771 /// `cargo test -p sozu-lib --locked mass_reap` (one positional filter —
15772 /// cargo rejects a second one). Every reaped stream is then queued
15773 /// unconditionally and `H2ControlTx::check_invariants` — the post-condition
15774 /// every mutating method on that type runs — panics inside the sweep with
15775 /// `pending RST queue must stay within its hard cap (escalates at the
15776 /// cap)`. That clause moved to the module with this commit and is NOT
15777 /// restated in `ConnectionH2::check_invariants`, so the explicit call
15778 /// below no longer covers the queue bound — the assertion at the end of
15779 /// this test is what pins it from the connection's side.
15780 #[test]
15781 fn mass_reap_keeps_the_pending_rst_queue_within_its_hard_cap() {
15782 // `Stream::new` checks out two buffers per stream and the connection
15783 // itself holds one for its zero buffer, so the ceiling has to clear
15784 // `2 * REAPED` with room to spare or `create_stream` hands back
15785 // `None` — which is how the first draft of this test failed.
15786 //
15787 // A production bound (`h2_control_tx::pending_rst_bound`, at least
15788 // `MIN_PENDING_RST_STREAMS`) would need thousands of streams and two
15789 // pool buffers each; what is under test is that the queue honours
15790 // its instance's bound, so the test installs a small one.
15791 let bound = 200;
15792 let reaped = bound + 32;
15793 let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 2 * reaped + 4, 16384)));
15794 let (mut connection, _peer) = test_h2_connection(&pool, None);
15795 connection.core.control_tx = h2_control_tx::H2ControlTx::with_cap(bound);
15796 let mut context = test_context(&pool);
15797 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
15798
15799 let idle_timeout = connection.core.stream_idle_timeout;
15800 let armed_at = connection.core.now;
15801 for i in 0..reaped {
15802 let gid = context
15803 .create_stream(Ulid::generate(), 1 << 16)
15804 .expect("test context must create a stream");
15805 // RFC 9113 §5.1.1: client-initiated stream ids are odd.
15806 connection
15807 .core
15808 .stream_table
15809 .register(2 * i as StreamId + 1, gid, armed_at);
15810 }
15811
15812 // One sweep, every stream past its deadline: the entire set is reaped
15813 // and RST-queued with no intervening flush.
15814 context.now = armed_at + idle_timeout + Duration::from_millis(1);
15815 connection.cancel_timed_out_streams(&mut context, &mut EndpointClient(&mut router));
15816
15817 #[cfg(debug_assertions)]
15818 connection.core.check_invariants(&context);
15819 assert!(
15820 connection.core.control_tx.pending().len() <= bound,
15821 "a mass reap must stop queueing at the cap, got {} entries",
15822 connection.core.control_tx.pending().len()
15823 );
15824 }
15825
15826 // ── flood accounting: refusals, provoked resets, the denominator ────
15827
15828 /// A peer that keeps opening streams Sōzu refuses neither grows the
15829 /// connection's state nor escapes the flood detector: a refused stream
15830 /// was never registered, so nothing ever removes its id from `rst_sent`,
15831 /// and each refusal past the limit counts as a glitch.
15832 ///
15833 /// The refusals here fall in the band back-pressure opens, local limit
15834 /// lowered below the advertised one, which cost nothing before.
15835 #[test]
15836 fn refused_streams_keep_rst_sent_bounded_and_trip_the_glitch_budget() {
15837 use std::io::Write;
15838
15839 const BATCHES: u32 = 50;
15840 const PER_BATCH: u32 = 1000;
15841 let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 8, 16384)));
15842 let (mut connection, mut peer) = test_h2_connection(&pool, None);
15843 let mut context = test_context(&pool);
15844 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
15845 connection.core.state = H2State::Header;
15846 connection
15847 .core
15848 .stream_table
15849 .set_expect_read(Some((H2StreamId::Zero, 9)));
15850 connection
15851 .core
15852 .local_settings
15853 .settings_max_concurrent_streams = 0;
15854 // `:method GET`, `:path /`, `:scheme https`, then `:authority`
15855 // as a literal without indexing: no HPACK table state to track.
15856 let mut block = vec![0x82, 0x84, 0x87, 0x01, 0x09];
15857 block.extend_from_slice(b"localhost");
15858
15859 let mut next_id: u32 = 1;
15860 'batches: for _ in 0..BATCHES {
15861 let mut wire = Vec::new();
15862 for _ in 0..PER_BATCH {
15863 wire.extend(orphan_frame(
15864 1,
15865 parser::FLAG_END_HEADERS | parser::FLAG_END_STREAM,
15866 next_id,
15867 block.len() as u32,
15868 &block,
15869 ));
15870 next_id += 2;
15871 }
15872 peer.write_all(&wire).expect("loopback write must complete");
15873 peer.flush().expect("loopback flush must complete");
15874 // One read pass handles one frame header or one payload: drive
15875 // until the batch's last stream id has been seen.
15876 let last_id = next_id - 2;
15877 for _ in 0..(16 * PER_BATCH) {
15878 connection.core.readiness.event.insert(Ready::READABLE);
15879 connection.core.readiness.interest.insert(Ready::READABLE);
15880 connection.readable(&mut context, EndpointClient(&mut router));
15881 let _ = connection.core.flush_pending_control_frames();
15882 connection.core.clear_output();
15883 if matches!(connection.core.state, H2State::GoAway | H2State::Error) {
15884 break 'batches;
15885 }
15886 if connection.core.stream_table.highest_peer_stream_id() >= last_id
15887 && matches!(connection.core.state, H2State::Header)
15888 {
15889 break;
15890 }
15891 }
15892 }
15893
15894 assert!(
15895 connection.core.stream_table.rst_sent_mut().len() < 100,
15896 "refused stream ids must not accumulate in rst_sent, got {}",
15897 connection.core.stream_table.rst_sent_mut().len()
15898 );
15899 assert!(
15900 matches!(connection.core.state, H2State::GoAway | H2State::Error),
15901 "endless refusals must trip the flood detector, got {:?} after {} streams",
15902 connection.core.state,
15903 next_id / 2
15904 );
15905 }
15906
15907 /// Every reset the peer provokes is a protocol error on its part and
15908 /// counts as a glitch, so provoked resets have a per-window rate limit
15909 /// independent of how many streams the connection served.
15910 #[test]
15911 fn peer_provoked_resets_count_as_glitches() {
15912 let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
15913 let (mut connection, _peer) = test_h2_connection(&pool, None);
15914 let now = connection.core.now;
15915 for _ in 0..1_000_000 {
15916 connection.core.flood_detector.record_stream_opened();
15917 }
15918 let glitch_budget = H2FloodConfig::default().max_glitch_count();
15919 let mut tripped_at = None;
15920 for n in 1..=(glitch_budget + 1) {
15921 connection.core.now = now;
15922 if connection
15923 .core
15924 .account_emitted_rst(H2Error::ProtocolError, RstOrigin::PeerProvoked)
15925 .is_some()
15926 {
15927 tripped_at = Some(n);
15928 break;
15929 }
15930 }
15931 assert_eq!(
15932 tripped_at,
15933 Some(glitch_budget + 1),
15934 "the provoked reset past the glitch budget must end the connection"
15935 );
15936 }
15937
15938 /// Streams the peer opens count toward the RST caps' denominator only
15939 /// once a backend answers them: a stream Sōzu refuses, answers itself or
15940 /// sees reset before a response cannot dilute the caps.
15941 #[test]
15942 fn unanswered_streams_do_not_dilute_the_reset_caps() {
15943 const FLOOR: u64 = 10;
15944 let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 70, 16384)));
15945 let (mut connection, _peer) = test_h2_connection(&pool, None);
15946 let mut context = test_context(&pool);
15947 connection.core.flood_detector = h2_flood_detector::H2FloodDetector::new(
15948 H2FloodConfig::from_optional(
15949 None,
15950 None,
15951 None,
15952 None,
15953 None,
15954 None,
15955 None,
15956 None,
15957 Some(FLOOR),
15958 None,
15959 None,
15960 None,
15961 None,
15962 None,
15963 ),
15964 connection.core.now,
15965 );
15966 for i in 0..30u32 {
15967 connection
15968 .core
15969 .create_stream(2 * i + 1, &mut context)
15970 .expect("the pool must hand out stream buffers");
15971 }
15972 let mut tripped_at = None;
15973 for n in 1..=(FLOOR + 1) {
15974 if connection
15975 .core
15976 .flood_detector
15977 .record_rst_received(false, false, connection.core.now)
15978 .is_some()
15979 {
15980 tripped_at = Some(n);
15981 break;
15982 }
15983 }
15984 assert_eq!(
15985 tripped_at,
15986 Some(FLOOR + 1),
15987 "thirty unanswered streams must not raise the pre-response cap"
15988 );
15989 }
15990
15991 /// Resets Sōzu emits on its own initiative — here the idle reaper's
15992 /// `RST_STREAM(CANCEL)` — are not charged to the peer: neither the
15993 /// CVE-2025-8671 MadeYouReset emitted-RST counter nor the pending RST
15994 /// queue's bound may accumulate them across drains.
15995 ///
15996 /// Two sweeps of 150 reaped streams with a control flush between them:
15997 /// 300 local resets in the connection's lifetime, never more than 150
15998 /// pending at once.
15999 ///
16000 /// TO SEE THIS RED: in `ConnectionH2::cancel_timed_out_streams`, pass
16001 /// `RstOrigin::PeerProvoked` instead of `RstOrigin::Local` to
16002 /// `ConnectionH2::enqueue_rst`. The emitted-RST counter then reads 300.
16003 #[test]
16004 fn local_resets_are_not_charged_to_the_peer() {
16005 const PER_SWEEP: usize = 150;
16006 let pool = Rc::new(RefCell::new(Pool::with_capacity(
16007 2,
16008 4 * PER_SWEEP + 4,
16009 16384,
16010 )));
16011 let (mut connection, _peer) = test_h2_connection(&pool, None);
16012 let mut context = test_context(&pool);
16013 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
16014 let idle_timeout = connection.core.stream_idle_timeout;
16015
16016 for sweep in 0..2 {
16017 let armed_at = connection.core.now;
16018 for i in 0..PER_SWEEP {
16019 let gid = context
16020 .create_stream(Ulid::generate(), 1 << 16)
16021 .expect("test context must create a stream");
16022 let wire_id = 2 * (sweep * PER_SWEEP + i) as StreamId + 1;
16023 connection
16024 .core
16025 .stream_table
16026 .register(wire_id, gid, armed_at);
16027 }
16028 context.now = armed_at + idle_timeout + Duration::from_millis(1);
16029 connection.cancel_timed_out_streams(&mut context, &mut EndpointClient(&mut router));
16030 // The SETTINGS-ACK watchdog is not under test.
16031 connection.core.settings_sent_at = None;
16032 let flushed = connection.core.flush_pending_control_frames();
16033 assert!(
16034 !matches!(flushed, H2ControlFlushTarget::Done(_)),
16035 "sweep {sweep}: local resets must not end the connection"
16036 );
16037 assert!(
16038 !matches!(connection.core.state, H2State::GoAway | H2State::Error),
16039 "sweep {sweep}: local resets must not send GOAWAY"
16040 );
16041 }
16042 assert_eq!(
16043 connection
16044 .core
16045 .flood_detector
16046 .total_rst_streams_emitted_lifetime(),
16047 0,
16048 "reaper CANCELs are Sōzu's decision, not the peer's"
16049 );
16050 }
16051
16052 // ── forcefully_terminate_answer arms WRITABLE for ET epoll ───────────
16053 //
16054 // Gap A in the h2spec diagnosis: the pre-fix code set `interest` but
16055 // never raised `event`, so `filter_interest() = event & interest` was
16056 // zero and `writable()` was never scheduled. This test pins the fix.
16057
16058 #[test]
16059 fn test_forcefully_terminate_answer_arms_event_and_interest() {
16060 let pool = make_pool_for_invariant_16();
16061 let ulid = Ulid::generate();
16062 let mut stream = make_stream_for_invariant_16(&pool, ulid);
16063 let mut readiness = Readiness::new();
16064
16065 assert!(!readiness.interest.is_writable());
16066 assert!(!readiness.event.is_writable());
16067
16068 forcefully_terminate_answer(&mut stream, &mut readiness, H2Error::ProtocolError);
16069
16070 assert!(
16071 readiness.interest.is_writable(),
16072 "forcefully_terminate_answer must set the WRITABLE interest bit"
16073 );
16074 assert!(
16075 readiness.event.is_writable(),
16076 "forcefully_terminate_answer must set the WRITABLE event bit — \
16077 without this, filter_interest() = 0 under edge-triggered epoll \
16078 and writable() is never scheduled (h2spec Gap A)"
16079 );
16080 }
16081
16082 // ── peer-address snapshot in the MUX-H2 log envelope ─────────────────
16083 //
16084 // `log_context!` / `log_context_stream!` render `peer=` from
16085 // `SocketHandler::peer_addr`, which is a snapshot the handler cached, not a
16086 // live `getpeername(2)`. Both tests below make the two answers differ on
16087 // purpose: the socket is genuinely connected to a loopback listener, so the
16088 // live lookup succeeds and would print `127.0.0.1:<port>`; only a macro
16089 // reading the snapshot can print `10.0.0.42:12345`.
16090 //
16091 // That gap is the PROXY-protocol symptom in miniature — the frontend's real
16092 // peer is the load balancer while the cache holds the advertised client —
16093 // and the same read is what keeps the slot populated after the peer's RST,
16094 // when `getpeername(2)` answers ENOTCONN and the live lookup collapses to
16095 // `None` on exactly the error lines an operator is reading.
16096
16097 /// A live, established loopback connection plus the address
16098 /// `getpeername(2)` reports for it. The listener is returned so the
16099 /// connection stays up for the whole test: these tests assert the snapshot
16100 /// wins even while the live lookup is perfectly healthy, so a half-dead
16101 /// socket would weaken them rather than strengthen them.
16102 fn connected_loopback_stream() -> (
16103 std::net::TcpListener,
16104 mio::net::TcpStream,
16105 std::net::SocketAddr,
16106 ) {
16107 let listener = std::net::TcpListener::bind("127.0.0.1:0")
16108 .expect("test listener must bind to a loopback port");
16109 let live_peer = listener
16110 .local_addr()
16111 .expect("test listener must report its local address");
16112 let stream =
16113 std::net::TcpStream::connect(live_peer).expect("loopback connect must complete");
16114 stream
16115 .set_nonblocking(true)
16116 .expect("mio requires a nonblocking stream");
16117 (listener, mio::net::TcpStream::from_std(stream), live_peer)
16118 }
16119
16120 /// Address the handler caches. Deliberately non-loopback so it cannot
16121 /// collide with whatever ephemeral port the live lookup would report.
16122 const CACHED_PEER: &str = "10.0.0.42:12345";
16123
16124 /// Build a server-side H2 connection whose frontend handler caches
16125 /// [`CACHED_PEER`] while its socket is really connected to `live_peer`.
16126 fn connection_with_cached_peer(
16127 pool: &Rc<RefCell<Pool>>,
16128 stream: mio::net::TcpStream,
16129 ) -> H2Shell<crate::socket::SessionTcpStream> {
16130 let session_ulid = Ulid::generate();
16131 let socket = crate::socket::SessionTcpStream::new(
16132 stream,
16133 session_ulid,
16134 Some(
16135 CACHED_PEER
16136 .parse()
16137 .expect("the cached peer literal must parse"),
16138 ),
16139 );
16140 H2Shell::new(
16141 session_ulid,
16142 socket,
16143 Position::Server,
16144 &mut PoolBufferSource::new(Rc::downgrade(pool)),
16145 H2FloodConfig::default(),
16146 H2ConnectionConfig::default(),
16147 Duration::from_secs(30),
16148 None,
16149 Duration::from_secs(30),
16150 Some((H2StreamId::Zero, CLIENT_PREFACE_SIZE)),
16151 Ready::READABLE | Ready::HUP | Ready::ERROR,
16152 )
16153 .expect("a pool with free buffers must yield an H2 connection")
16154 }
16155
16156 /// To SEE THIS RED: in `log_context!` (h2.rs), put
16157 /// `peer = $self.socket.socket_ref().peer_addr().ok(),` back in place of
16158 /// `peer = $self.peer_address,`. The rendered line then carries the
16159 /// loopback address the socket is really connected to, so the first
16160 /// assertion fails on the missing cached address.
16161 ///
16162 /// Substituting the pre-snapshot `peer = $self.socket.peer_addr(),` does
16163 /// NOT turn this red, and that is worth stating: for a handler that
16164 /// already caches, reading the cache and reading a snapshot taken from
16165 /// that cache agree by construction. What the snapshot changed is WHICH
16166 /// object is read on a log line, not what comes back — so the call-count
16167 /// property, not this one, is what distinguishes them. It is pinned by
16168 /// [`log_context_reads_the_peer_address_once_per_connection`].
16169 #[test]
16170 fn log_context_renders_the_cached_peer_not_a_live_lookup() {
16171 let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
16172 let (_listener, stream, live_peer) = connected_loopback_stream();
16173 let connection = connection_with_cached_peer(&pool, stream);
16174
16175 // Premise of the test: the live lookup is healthy and disagrees with the
16176 // cache. Without this the assertions below could pass for the wrong
16177 // reason (both answers happening to be the same address).
16178 assert_eq!(
16179 connection.socket.socket_ref().peer_addr().ok(),
16180 Some(live_peer),
16181 "the test socket must be genuinely connected, so a live lookup succeeds"
16182 );
16183
16184 let rendered = log_context!(connection.core);
16185
16186 assert!(
16187 rendered.contains(&format!("peer=Some({CACHED_PEER})")),
16188 "MUX-H2 context must render the cached peer: {rendered}"
16189 );
16190 assert!(
16191 !rendered.contains(&live_peer.to_string()),
16192 "MUX-H2 context must not fall back to the live getpeername(2) answer: {rendered}"
16193 );
16194 }
16195
16196 /// To SEE THIS RED: in `log_context_stream!` (h2.rs), put
16197 /// `peer = $self.socket.socket_ref().peer_addr().ok(),` back in place of
16198 /// `peer = $self.peer_address,`. The per-stream envelope then
16199 /// diverges from the connection envelope, which is worse than either being
16200 /// wrong alone: the same session renders two different peers depending on
16201 /// whether a stream happened to be in scope at the callsite.
16202 #[test]
16203 fn log_context_stream_renders_the_cached_peer_not_a_live_lookup() {
16204 let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 20, 16_384)));
16205 let (_listener, stream, live_peer) = connected_loopback_stream();
16206 let connection = connection_with_cached_peer(&pool, stream);
16207 let mux_stream = make_stream_for_invariant_16(&pool, connection.core.session_ulid);
16208 let http_context = &mux_stream.context;
16209
16210 assert_eq!(
16211 connection.socket.socket_ref().peer_addr().ok(),
16212 Some(live_peer),
16213 "the test socket must be genuinely connected, so a live lookup succeeds"
16214 );
16215
16216 let rendered = log_context_stream!(connection.core, http_context);
16217
16218 assert!(
16219 rendered.contains(&format!("peer=Some({CACHED_PEER})")),
16220 "per-stream MUX-H2 context must render the cached peer: {rendered}"
16221 );
16222 assert!(
16223 !rendered.contains(&live_peer.to_string()),
16224 "per-stream MUX-H2 context must not fall back to the live \
16225 getpeername(2) answer: {rendered}"
16226 );
16227 }
16228
16229 /// A [`SocketHandler`] that counts how often it is asked for the peer
16230 /// address, delegating every other method to a real loopback stream.
16231 ///
16232 /// The two tests above pin WHICH address the log prefix renders. Neither
16233 /// can see how many times the connection reaches into the socket to get
16234 /// it, because both production handlers answer from a cache and so give
16235 /// the same answer however often they are asked. This handler makes the
16236 /// count observable.
16237 struct PeerAddrCountingSocket {
16238 stream: mio::net::TcpStream,
16239 peer: std::net::SocketAddr,
16240 calls: Rc<std::cell::Cell<usize>>,
16241 }
16242
16243 impl SocketHandler for PeerAddrCountingSocket {
16244 fn socket_read(&mut self, buf: &mut [u8]) -> (usize, SocketResult) {
16245 self.stream.socket_read(buf)
16246 }
16247
16248 fn socket_write(&mut self, buf: &[u8]) -> (usize, SocketResult) {
16249 self.stream.socket_write(buf)
16250 }
16251
16252 fn socket_write_vectored(&mut self, bufs: &[IoSlice]) -> (usize, SocketResult) {
16253 self.stream.socket_write_vectored(bufs)
16254 }
16255
16256 fn socket_ref(&self) -> &mio::net::TcpStream {
16257 &self.stream
16258 }
16259
16260 fn socket_mut(&mut self) -> &mut mio::net::TcpStream {
16261 &mut self.stream
16262 }
16263
16264 fn peer_addr(&self) -> Option<std::net::SocketAddr> {
16265 self.calls.set(self.calls.get() + 1);
16266 Some(self.peer)
16267 }
16268
16269 fn protocol(&self) -> crate::socket::TransportProtocol {
16270 crate::socket::TransportProtocol::Tcp
16271 }
16272
16273 fn read_error(&self) {}
16274
16275 fn write_error(&self) {}
16276 }
16277
16278 /// The `peer=` slot is read from the socket exactly ONCE per connection —
16279 /// at construction — however many log lines the connection renders.
16280 ///
16281 /// This is the property that makes `peer_address` worth having. It is not
16282 /// about which address appears (the two tests above own that); it is about
16283 /// `log_context!` no longer being a socket operation. The macro expands at
16284 /// 113 production callsites in this file, so before the snapshot every
16285 /// method that logged was `Front`-coupled whether or not it did any I/O —
16286 /// which is 113 of the ~140 total `Front` touch points in `h2.rs`.
16287 ///
16288 /// TO SEE THIS RED: in `log_context!` (h2.rs), put
16289 /// `peer = $self.socket.peer_addr(),` back in place of
16290 /// `peer = $self.peer_address,`. The count then rises by one per rendered
16291 /// line and the final assertion fails with
16292 /// `the peer address must be read once, at construction, not once per log line:
16293 /// left: 4, right: 1`.
16294 #[test]
16295 fn log_context_reads_the_peer_address_once_per_connection() {
16296 let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
16297 let (_listener, stream, _live_peer) = connected_loopback_stream();
16298 let calls = Rc::new(std::cell::Cell::new(0usize));
16299 let session_ulid = Ulid::generate();
16300 let socket = PeerAddrCountingSocket {
16301 stream,
16302 peer: CACHED_PEER
16303 .parse()
16304 .expect("the cached peer literal must parse"),
16305 calls: Rc::clone(&calls),
16306 };
16307 let connection = H2Shell::new(
16308 session_ulid,
16309 socket,
16310 Position::Server,
16311 &mut PoolBufferSource::new(Rc::downgrade(&pool)),
16312 H2FloodConfig::default(),
16313 H2ConnectionConfig::default(),
16314 Duration::from_secs(30),
16315 None,
16316 Duration::from_secs(30),
16317 Some((H2StreamId::Zero, CLIENT_PREFACE_SIZE)),
16318 Ready::READABLE | Ready::HUP | Ready::ERROR,
16319 )
16320 .expect("a pool with free buffers must yield an H2 connection");
16321
16322 assert_eq!(
16323 calls.get(),
16324 1,
16325 "construction takes exactly one peer_addr() snapshot"
16326 );
16327
16328 // Premise: the rendered lines really do carry the address, so a zero
16329 // count below would mean the slot went missing rather than that it
16330 // became free.
16331 for _ in 0..3 {
16332 let rendered = log_context!(connection.core);
16333 assert!(
16334 rendered.contains(&format!("peer=Some({CACHED_PEER})")),
16335 "each rendered line must still carry the peer: {rendered}"
16336 );
16337 }
16338
16339 assert_eq!(
16340 calls.get(),
16341 1,
16342 "the peer address must be read once, at construction, not once per log line"
16343 );
16344 }
16345
16346 // ── The read-side two-call protocol (poll_read_target / handle_read) ──
16347
16348 /// `read_space` hands out the buffer the core named, and hands out the
16349 /// stream's READ buffer — the one `Stream::split` labels `rbuffer` for the
16350 /// connection's position — not its write buffer.
16351 ///
16352 /// This is the only genuinely new logic in the read-side inversion: every
16353 /// other line moved. Both halves are pinned because the two failure modes
16354 /// differ — offering the wrong *kawa* corrupts the peer's request while
16355 /// the response is parsed as H2 payload, offering the wrong *stream*
16356 /// writes one stream's DATA into another's.
16357 ///
16358 /// To SEE THIS RED: in `read_buffer` (h2.rs), return
16359 /// `streams[global_stream_id].split(position).wbuffer` in place of
16360 /// `.rbuffer`.
16361 #[test]
16362 fn read_space_offers_the_read_buffer_of_the_stream_the_core_named() {
16363 let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 20, 16_384)));
16364 let (mut connection, _peer) = test_h2_connection(&pool, None);
16365 let mut context = test_context(&pool);
16366
16367 let gid = context
16368 .create_stream(Ulid::generate(), 1 << 16)
16369 .expect("test context must create a stream");
16370
16371 // Premise: the two per-stream buffers really are distinct allocations,
16372 // so a pointer comparison below can tell them apart at all.
16373 let front = context.streams[gid].front.storage.space().as_ptr();
16374 let back = context.streams[gid].back.storage.space().as_ptr();
16375 let zero = connection.core.zero.storage.space().as_ptr();
16376 assert_ne!(
16377 front, back,
16378 "a stream's request and response buffers must be distinct allocations"
16379 );
16380
16381 let offered = read_space(
16382 &mut connection.core.zero,
16383 &mut context.streams,
16384 &connection.core.position,
16385 H2StreamId::Zero,
16386 9,
16387 );
16388 assert_eq!(
16389 offered.as_ptr(),
16390 zero,
16391 "H2StreamId::Zero must offer the connection-level scratch buffer"
16392 );
16393 assert_eq!(
16394 offered.len(),
16395 9,
16396 "the offer is capped at the byte debt, never the whole free space"
16397 );
16398
16399 let offered = read_space(
16400 &mut connection.core.zero,
16401 &mut context.streams,
16402 &connection.core.position,
16403 H2StreamId::Other { id: 1, gid },
16404 7,
16405 );
16406 assert_eq!(
16407 offered.as_ptr(),
16408 front,
16409 "a server position must offer the stream's request (front) buffer"
16410 );
16411 assert_ne!(
16412 offered.as_ptr(),
16413 back,
16414 "the offered buffer must not be the stream's response (back) buffer"
16415 );
16416 assert_eq!(
16417 offered.len(),
16418 7,
16419 "the offer is capped at the byte debt, never the whole free space"
16420 );
16421 }
16422
16423 /// A frame whose payload is zero bytes long owes the socket nothing, and
16424 /// the core says so with its own variant rather than an `amount` of 0.
16425 ///
16426 /// That distinction is load-bearing, not cosmetic. A caller handed
16427 /// `Fill { amount: 0 }` would issue a zero-length read, and
16428 /// `update_readiness_after_read(0, SocketResult::Continue, ..)` returns
16429 /// `true` for it — "nothing arrived, stop" — so `handle_read` would return
16430 /// before the frame state machine ever ran, and every empty SETTINGS,
16431 /// empty DATA and SETTINGS ACK would stop being parsed.
16432 ///
16433 /// To SEE THIS RED: in `poll_read_target` (h2.rs), replace the trailing
16434 /// `if amount > 0 { .. } else { .. }` with its `Fill` half alone —
16435 /// keep the `available_space` guard and end the function with
16436 /// `H2ReadTarget::Fill { stream_id, amount }`, dropping the
16437 /// `set_expect_read(None)` / `H2ReadTarget::Skip(stream_id)` branch.
16438 #[test]
16439 fn poll_read_target_skips_the_read_when_the_frame_carries_no_payload() {
16440 let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
16441 let (mut connection, _peer) = test_h2_connection(&pool, None);
16442 let mut context = test_context(&pool);
16443 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
16444
16445 connection.core.state = H2State::Header;
16446 connection
16447 .core
16448 .stream_table
16449 .set_expect_read(Some((H2StreamId::Zero, 0)));
16450
16451 let target = connection
16452 .core
16453 .poll_read_target(&mut context, &mut EndpointClient(&mut router));
16454 assert!(
16455 matches!(target, H2ReadTarget::Skip(H2StreamId::Zero)),
16456 "a zero-length payload must be a Skip, not a zero-length Fill: {target:?}"
16457 );
16458 assert!(
16459 connection.core.stream_table.expect_read().is_none(),
16460 "the settled byte debt must be cleared before the frame is dispatched"
16461 );
16462 }
16463
16464 /// The core is told how many bytes arrived, and it subtracts exactly that
16465 /// many from the debt it offered — a short read leaves the remainder owed,
16466 /// it does not restart the frame.
16467 ///
16468 /// 6 is neither of the two numbers the test hands in, so this cannot pass
16469 /// by echoing the fixture: `amount` is 9 and `size` is 3.
16470 ///
16471 /// To SEE THIS RED: in `handle_read` (h2.rs), pass `amount` in place of
16472 /// `amount - size` to `set_expect_read`.
16473 #[test]
16474 fn handle_read_subtracts_the_byte_count_the_caller_reported() {
16475 let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
16476 let (mut connection, _peer) = test_h2_connection(&pool, None);
16477 let mut context = test_context(&pool);
16478 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
16479
16480 // Not ClientPreface: a short read there runs the early-preface check
16481 // instead, which is a different branch with its own coverage.
16482 connection.core.state = H2State::Header;
16483 connection
16484 .core
16485 .stream_table
16486 .set_expect_read(Some((H2StreamId::Zero, 9)));
16487
16488 let result = connection.core.handle_read(
16489 &mut context,
16490 EndpointClient(&mut router),
16491 H2StreamId::Zero,
16492 H2ReadOutcome::Filled {
16493 amount: 9,
16494 size: 3,
16495 status: SocketResult::Continue,
16496 },
16497 );
16498
16499 assert!(
16500 matches!(result, MuxResult::Continue),
16501 "a short read keeps the connection running: {result:?}"
16502 );
16503 assert_eq!(
16504 connection.core.stream_table.expect_read(),
16505 Some((H2StreamId::Zero, 6)),
16506 "a 3-byte answer to a 9-byte offer must leave 6 bytes owed"
16507 );
16508 assert_eq!(
16509 connection.core.zero.storage.data().len(),
16510 3,
16511 "the core must consume exactly the bytes the caller reported"
16512 );
16513 }
16514
16515 /// A read that returned nothing clears the READABLE **event** and leaves
16516 /// the READABLE **interest** alone, so the connection is re-armed for the
16517 /// next epoll wake-up instead of being taken off the loop.
16518 ///
16519 /// The interest half is asserted on purpose: `signal_pending_write` and
16520 /// this path both touch `Readiness.event` only, and an assertion written
16521 /// against `interest` cannot see either of them move.
16522 ///
16523 /// To SEE THIS RED: in `handle_read` (h2.rs), call
16524 /// `update_readiness_after_write` in place of `update_readiness_after_read`
16525 /// — the WRITABLE bit is cleared instead and the READABLE event survives.
16526 #[test]
16527 fn handle_read_clears_the_readable_event_not_the_interest_when_nothing_arrived() {
16528 let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
16529 let (mut connection, _peer) = test_h2_connection(&pool, None);
16530 let mut context = test_context(&pool);
16531 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
16532
16533 connection.core.state = H2State::Header;
16534 connection
16535 .core
16536 .stream_table
16537 .set_expect_read(Some((H2StreamId::Zero, 9)));
16538 connection.core.readiness.event = Ready::READABLE;
16539 connection.core.readiness.interest = Ready::READABLE | Ready::HUP | Ready::ERROR;
16540
16541 let result = connection.core.handle_read(
16542 &mut context,
16543 EndpointClient(&mut router),
16544 H2StreamId::Zero,
16545 H2ReadOutcome::Filled {
16546 amount: 9,
16547 size: 0,
16548 status: SocketResult::WouldBlock,
16549 },
16550 );
16551
16552 assert!(
16553 matches!(result, MuxResult::Continue),
16554 "an empty read is not a close: {result:?}"
16555 );
16556 assert!(
16557 !connection.core.readiness.event.is_readable(),
16558 "an empty read must clear the READABLE event"
16559 );
16560 assert!(
16561 connection.core.readiness.interest.is_readable(),
16562 "an empty read must NOT drop the READABLE interest"
16563 );
16564 assert_eq!(
16565 connection.core.stream_table.expect_read(),
16566 Some((H2StreamId::Zero, 9)),
16567 "an empty read settles no part of the byte debt"
16568 );
16569 }
16570
16571 // ── RFC 9113 §4.3: a refused stream must not desynchronise HPACK ──────
16572 //
16573 // Field compression state is scoped to the whole connection, not to a
16574 // stream. When `handle_header_state` refuses a stream — during a
16575 // graceful drain, over MAX_CONCURRENT_STREAMS, or on buffer-pool
16576 // exhaustion — the HEADERS payload it drops is an HPACK field block the
16577 // peer's encoder has *already* applied to its own dynamic table. Dropping
16578 // it without decoding leaves our decoder permanently behind the peer's
16579 // encoder, and every later header block on the surviving connection then
16580 // resolves the wrong dynamic entry or fails outright.
16581
16582 /// To SEE THIS RED: in the `(H2State::Discard, _)` arm of
16583 /// [`ConnectionH2::handle_read`], remove the `if let Some(discarded) =
16584 /// self.discarded_field_block.take() && ...` block that calls
16585 /// [`decode_discarded_field_block`], restoring the unconditional
16586 /// `kawa.storage.clear()`. The peer's second block is then a one-byte
16587 /// reference to dynamic index 62 that our decoder has never been told
16588 /// about, and the decode fails with an out-of-table index. Verified
16589 /// 2026-09-21 (see this commit's message for the exact failure output).
16590 #[test]
16591 fn a_refused_stream_keeps_the_hpack_decoder_in_sync() {
16592 use std::io::Write;
16593
16594 let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 20, 16_384)));
16595 let (mut connection, mut peer) = test_h2_connection(&pool, None);
16596 let mut context = test_context(&pool);
16597 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
16598
16599 // Past the preface/SETTINGS handshake, waiting on a frame header, and
16600 // draining — so the next client-initiated stream is refused rather
16601 // than created. Drain is only the cheapest of the three refusal
16602 // triggers; MAX_CONCURRENT_STREAMS and pool exhaustion reach the same
16603 // `refuse_stream_and_discard` call.
16604 connection.core.state = H2State::Header;
16605 connection
16606 .core
16607 .stream_table
16608 .set_expect_read(Some((H2StreamId::Zero, 9)));
16609 connection.core.drain.__test_set_draining();
16610
16611 // The peer's encoder. It indexes a header whose *name* is in
16612 // neither table, so this block appends `x-sozu-probe: alpha` to the
16613 // peer's dynamic table at index 62.
16614 let mut peer_encoder = crate::protocol::mux::hpack::Encoder::new();
16615 let refused_block = peer_encoder.encode([
16616 (&b":method"[..], &b"GET"[..]),
16617 (&b":scheme"[..], &b"https"[..]),
16618 (&b":authority"[..], &b"example.com"[..]),
16619 (&b":path"[..], &b"/refused"[..]),
16620 (&b"x-sozu-probe"[..], &b"alpha"[..]),
16621 ]);
16622 assert!(
16623 refused_block.len() < 16_384,
16624 "the probe block must fit one HEADERS frame under the default max frame size"
16625 );
16626
16627 // HEADERS, END_STREAM | END_HEADERS, stream 1.
16628 let mut frame = Vec::with_capacity(9 + refused_block.len());
16629 frame.extend_from_slice(&(refused_block.len() as u32).to_be_bytes()[1..]);
16630 frame.push(1);
16631 frame.push(parser::FLAG_END_STREAM | parser::FLAG_END_HEADERS);
16632 frame.extend_from_slice(&1u32.to_be_bytes());
16633 frame.extend_from_slice(&refused_block);
16634 peer.write_all(&frame)
16635 .expect("loopback write must complete");
16636 peer.flush().expect("loopback flush must complete");
16637
16638 // Two passes consume the 9-byte header and then the payload; the rest
16639 // are `WouldBlock` no-ops. Bounded so a delivery hiccup fails loudly
16640 // instead of hanging.
16641 for _ in 0..64 {
16642 connection.readable(&mut context, EndpointClient(&mut router));
16643 if matches!(connection.core.state, H2State::Header)
16644 && matches!(
16645 connection.core.stream_table.expect_read(),
16646 Some((H2StreamId::Zero, 9))
16647 )
16648 && !connection.core.control_tx.pending().is_empty()
16649 {
16650 break;
16651 }
16652 std::thread::yield_now();
16653 }
16654
16655 // Premise of the test: we really went through the refusal path, the
16656 // stream was refused rather than created, and the connection survived.
16657 assert_eq!(
16658 connection.core.control_tx.pending(),
16659 vec![(1, H2Error::RefusedStream)],
16660 "the drain gate must refuse stream 1 with RST_STREAM(REFUSED_STREAM)"
16661 );
16662 assert!(
16663 connection.core.stream_table.is_empty(),
16664 "a refused stream must not be created"
16665 );
16666 assert!(
16667 !matches!(connection.core.state, H2State::Error | H2State::GoAway),
16668 "refusing one stream must leave the connection usable: {:?}",
16669 connection.core.state
16670 );
16671
16672 // The peer's next block references the dynamic entry its encoder added
16673 // while encoding the refused request — a single indexed field.
16674 let next_block = peer_encoder.encode([(&b"x-sozu-probe"[..], &b"alpha"[..])]);
16675 assert_eq!(
16676 next_block.len(),
16677 1,
16678 "the peer must now reference its dynamic entry, not re-send a literal"
16679 );
16680
16681 let mut decoded = Vec::new();
16682 let status = connection
16683 .core
16684 .hpack
16685 .decoder_mut()
16686 .decode_with_cb(&next_block, |k, v| {
16687 decoded.push((k.into_owned(), v.into_owned()));
16688 });
16689
16690 assert!(
16691 status.is_ok(),
16692 "the connection decoder must still resolve the peer's dynamic table \
16693 after a refused stream, got {status:?}"
16694 );
16695 assert_eq!(
16696 decoded,
16697 vec![(b"x-sozu-probe".to_vec(), b"alpha".to_vec())],
16698 "the refused stream's field block must have been decoded into the \
16699 connection's HPACK context"
16700 );
16701 }
16702
16703 /// Serialise one frame header followed by `payload`.
16704 fn orphan_frame(
16705 frame_type: u8,
16706 flags: u8,
16707 stream_id: u32,
16708 len: u32,
16709 payload: &[u8],
16710 ) -> Vec<u8> {
16711 let mut frame = Vec::with_capacity(9 + payload.len());
16712 frame.extend_from_slice(&len.to_be_bytes()[1..]);
16713 frame.push(frame_type);
16714 frame.push(flags);
16715 frame.extend_from_slice(&stream_id.to_be_bytes());
16716 frame.extend_from_slice(payload);
16717 frame
16718 }
16719
16720 /// A PING the peer sends after the orphaned payload: once its ACK sits in
16721 /// `zero`, the connection has provably read past that payload.
16722 fn orphan_ping() -> Vec<u8> {
16723 orphan_frame(6, 0, 0, 8, b"pingpong")
16724 }
16725
16726 /// Open stream 1 with a request whose body follows, then send a DATA
16727 /// header announcing `payload_len` bytes and the first `sent` of them,
16728 /// and drive `readable()` until the connection waits on that payload.
16729 /// Returns what `expect_read` then names.
16730 fn open_stream_mid_data_payload(
16731 connection: &mut H2Shell<mio::net::TcpStream>,
16732 peer: &mut std::net::TcpStream,
16733 context: &mut Context<crate::protocol::mux::test_support::TestListener>,
16734 router: &mut Router,
16735 payload_len: u32,
16736 sent: usize,
16737 ) -> Option<(H2StreamId, usize)> {
16738 use std::io::Write;
16739
16740 connection.core.state = H2State::Header;
16741 connection
16742 .core
16743 .stream_table
16744 .set_expect_read(Some((H2StreamId::Zero, 9)));
16745 let block = crate::protocol::mux::hpack::Encoder::new().encode([
16746 (&b":method"[..], &b"POST"[..]),
16747 (&b":scheme"[..], &b"https"[..]),
16748 (&b":authority"[..], &b"example.com"[..]),
16749 (&b":path"[..], &b"/upload"[..]),
16750 ]);
16751 let mut bytes = orphan_frame(1, parser::FLAG_END_HEADERS, 1, block.len() as u32, &block);
16752 bytes.extend(orphan_frame(0, 0, 1, payload_len, &vec![b'x'; sent]));
16753 peer.write_all(&bytes)
16754 .expect("loopback write must complete");
16755 peer.flush().expect("loopback flush must complete");
16756 for _ in 0..64 {
16757 connection.core.readiness.event.insert(Ready::READABLE);
16758 connection.readable(context, EndpointClient(router));
16759 if let Some((H2StreamId::Other { .. }, remaining)) =
16760 connection.core.stream_table.expect_read()
16761 && (remaining == payload_len as usize - sent
16762 || !connection.core.readiness.interest.is_readable())
16763 {
16764 break;
16765 }
16766 std::thread::yield_now();
16767 }
16768 connection.core.stream_table.expect_read()
16769 }
16770
16771 /// Drive `readable()` until `done` holds, re-inserting the READABLE event
16772 /// AND interest before every pass, so a lost edge-triggered event cannot
16773 /// be what stalls the connection. Returns whether `done` held.
16774 fn drive_reads_with_readiness_forced(
16775 connection: &mut H2Shell<mio::net::TcpStream>,
16776 context: &mut Context<crate::protocol::mux::test_support::TestListener>,
16777 router: &mut Router,
16778 done: impl Fn(&H2Shell<mio::net::TcpStream>) -> bool,
16779 ) -> bool {
16780 for _ in 0..256 {
16781 connection.core.readiness.event.insert(Ready::READABLE);
16782 connection.core.readiness.interest.insert(Ready::READABLE);
16783 connection.readable(context, EndpointClient(router));
16784 if done(connection) {
16785 return true;
16786 }
16787 std::thread::yield_now();
16788 }
16789 false
16790 }
16791
16792 /// Regression test for #1597: a stream reaped by the per-stream idle
16793 /// guard while it owns the DATA payload being read must not leave the
16794 /// connection unable to read.
16795 ///
16796 /// `H2StreamTable::remove` nulls an `expect_read` naming the removed
16797 /// stream. Before the fix nothing else changed, so the connection stayed
16798 /// in `H2State::Frame` with nothing to read, and every later `readable()`
16799 /// returned at the `expect_read() == None` early exit — the remaining
16800 /// payload, the PING behind it and every later frame were never read.
16801 ///
16802 /// TO SEE THIS RED: in [`ConnectionH2::remove_dead_stream`], delete the
16803 /// `skip_orphaned_data_payload` call. The test fails with `after the reap
16804 /// the connection must still read the PING behind the orphaned payload;
16805 /// state=Frame(FrameHeader { payload_len: 10, frame_type: Data, flags: 0,
16806 /// stream_id: 1 }) expect_read=None`. Verified 2026-09-26.
16807 #[test]
16808 fn reaping_a_stream_mid_data_payload_keeps_the_connection_reading() {
16809 use std::io::Write;
16810
16811 let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 20, 16_384)));
16812 let (mut connection, mut peer) = test_h2_connection(&pool, None);
16813 let mut context = test_context(&pool);
16814 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
16815
16816 let expect = open_stream_mid_data_payload(
16817 &mut connection,
16818 &mut peer,
16819 &mut context,
16820 &mut router,
16821 10,
16822 4,
16823 );
16824 assert!(
16825 matches!(expect, Some((H2StreamId::Other { .. }, 6))),
16826 "premise: mid DATA payload with 6 bytes left, got {expect:?}"
16827 );
16828
16829 // The peer pauses past `stream_idle_timeout` (30 s in
16830 // `test_h2_connection`), then sends the rest and a PING.
16831 context.now += Duration::from_secs(31);
16832 let mut rest = b"efghij".to_vec();
16833 rest.extend(orphan_ping());
16834 peer.write_all(&rest).expect("loopback write must complete");
16835 peer.flush().expect("loopback flush must complete");
16836
16837 let read_on =
16838 drive_reads_with_readiness_forced(&mut connection, &mut context, &mut router, |c| {
16839 !c.core.output.is_empty()
16840 });
16841 assert!(
16842 read_on,
16843 "after the reap the connection must still read the PING behind the \
16844 orphaned payload; state={:?} expect_read={:?}",
16845 connection.core.state,
16846 connection.core.stream_table.expect_read()
16847 );
16848 assert!(
16849 connection.core.stream_table.is_empty(),
16850 "premise: the idle guard reaped stream 1"
16851 );
16852 }
16853
16854 /// #1597: the orphaned frame never reaches `handle_data_frame`, so its
16855 /// whole wire length must still be credited to the connection receive
16856 /// window (RFC 9113 §6.9), or every reap shrinks the peer's window.
16857 ///
16858 /// TO SEE THIS RED: in [`ConnectionH2::skip_orphaned_data_payload`],
16859 /// delete the `account_received_bytes` block. The test then fails with
16860 /// `the whole orphaned DATA frame must be credited to the connection
16861 /// window`. Verified 2026-09-26.
16862 #[test]
16863 fn a_skipped_orphaned_data_payload_is_credited_to_the_connection_window() {
16864 use std::io::Write;
16865
16866 let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 20, 16_384)));
16867 let (mut connection, mut peer) = test_h2_connection(&pool, None);
16868 let mut context = test_context(&pool);
16869 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
16870
16871 let expect = open_stream_mid_data_payload(
16872 &mut connection,
16873 &mut peer,
16874 &mut context,
16875 &mut router,
16876 10,
16877 4,
16878 );
16879 assert!(matches!(expect, Some((H2StreamId::Other { .. }, 6))));
16880 context.now += Duration::from_secs(31);
16881 let mut rest = b"efghij".to_vec();
16882 rest.extend(orphan_ping());
16883 peer.write_all(&rest).expect("loopback write must complete");
16884 peer.flush().expect("loopback flush must complete");
16885 assert!(drive_reads_with_readiness_forced(
16886 &mut connection,
16887 &mut context,
16888 &mut router,
16889 |c| !c.core.output.is_empty(),
16890 ));
16891
16892 // A threshold of 1 hands back everything accumulated since the last
16893 // connection WINDOW_UPDATE: exactly the orphaned frame's length.
16894 assert_eq!(
16895 connection.core.flow_control.account_received_bytes(0, 1),
16896 Some(10),
16897 "the whole orphaned DATA frame must be credited to the connection window"
16898 );
16899 }
16900
16901 /// #1597: a remainder close to `max_frame_size` is skipped in pieces as
16902 /// small as the room in `zero` allows, never by waiting for room for all
16903 /// of it. With 4 KiB buffers a 16 KiB remainder can never fit at once.
16904 ///
16905 /// TO SEE THIS RED: in [`ConnectionH2::poll_read_target`], replace
16906 /// `amount.min(available)` by `amount`. The connection then parks for
16907 /// good on the full remainder and the test fails with `the orphaned
16908 /// remainder must be skipped in pieces`.
16909 #[test]
16910 fn a_remainder_near_max_frame_size_is_skipped_in_pieces() {
16911 use std::io::Write;
16912
16913 const PAYLOAD_LEN: u32 = 16_384;
16914
16915 let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 20, 4_096)));
16916 let (mut connection, mut peer) = test_h2_connection(&pool, None);
16917 let mut context = test_context(&pool);
16918 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
16919
16920 let expect = open_stream_mid_data_payload(
16921 &mut connection,
16922 &mut peer,
16923 &mut context,
16924 &mut router,
16925 PAYLOAD_LEN,
16926 0,
16927 );
16928 assert!(
16929 matches!(expect, Some((H2StreamId::Other { .. }, n)) if n == PAYLOAD_LEN as usize),
16930 "premise: waiting on the whole DATA payload, got {expect:?}"
16931 );
16932 assert!(
16933 connection.core.zero.storage.capacity() < PAYLOAD_LEN as usize,
16934 "premise: the remainder cannot fit in `zero` at once"
16935 );
16936
16937 context.now += Duration::from_secs(31);
16938 let mut rest = vec![b'x'; PAYLOAD_LEN as usize];
16939 rest.extend(orphan_ping());
16940 peer.write_all(&rest).expect("loopback write must complete");
16941 peer.flush().expect("loopback flush must complete");
16942
16943 let read_on =
16944 drive_reads_with_readiness_forced(&mut connection, &mut context, &mut router, |c| {
16945 !c.core.output.is_empty()
16946 });
16947 assert!(
16948 read_on,
16949 "the orphaned remainder must be skipped in pieces; state={:?} expect_read={:?}",
16950 connection.core.state,
16951 connection.core.stream_table.expect_read()
16952 );
16953 assert!(
16954 matches!(connection.core.state, H2State::Header),
16955 "the connection must be back on frame headers, not in {:?}",
16956 connection.core.state
16957 );
16958 assert_eq!(
16959 connection.core.flow_control.account_received_bytes(0, 1),
16960 Some(PAYLOAD_LEN),
16961 "the whole orphaned DATA frame must be credited to the connection window"
16962 );
16963 }
16964
16965 /// #1597, parked form: a stream parked on buffer pressure (READABLE
16966 /// interest removed until `try_resume_reading` sees room) and then
16967 /// reaped by `Mux::timeout`'s `cancel_timed_out_streams` must resume
16968 /// reading. `try_resume_reading` only resumes a stream that still
16969 /// exists, so nothing else restores the interest.
16970 ///
16971 /// Unlike the other #1597 tests, this one never re-inserts READABLE
16972 /// interest itself: it calls `readable()` only when the mux would.
16973 ///
16974 /// TO SEE THIS RED: in [`ConnectionH2::skip_orphaned_data_payload`],
16975 /// delete `self.readiness.interest.insert(Ready::READABLE);`. The test
16976 /// fails with `a reaped parked stream must not leave the connection
16977 /// unable to read`.
16978 #[test]
16979 fn reaping_a_stream_parked_on_buffer_pressure_resumes_reading() {
16980 use std::io::Write;
16981
16982 const PAYLOAD_LEN: u32 = 16_384;
16983
16984 let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 20, 4_096)));
16985 let (mut connection, mut peer) = test_h2_connection(&pool, None);
16986 let mut context = test_context(&pool);
16987 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
16988
16989 let expect = open_stream_mid_data_payload(
16990 &mut connection,
16991 &mut peer,
16992 &mut context,
16993 &mut router,
16994 PAYLOAD_LEN,
16995 0,
16996 );
16997 assert!(
16998 matches!(expect, Some((H2StreamId::Other { .. }, n)) if n == PAYLOAD_LEN as usize),
16999 "premise: waiting on the whole DATA payload, got {expect:?}"
17000 );
17001 // The next pass finds no room for the frame in the stream buffer.
17002 connection.readable(&mut context, EndpointClient(&mut router));
17003 assert!(
17004 !connection.core.readiness.interest.is_readable(),
17005 "premise: the stream buffer cannot take the frame, so the read parked"
17006 );
17007
17008 let mut rest = vec![b'x'; PAYLOAD_LEN as usize];
17009 rest.extend(orphan_ping());
17010 peer.write_all(&rest).expect("loopback write must complete");
17011 peer.flush().expect("loopback flush must complete");
17012
17013 // `Mux::timeout` reaps the parked stream.
17014 context.now += Duration::from_secs(31);
17015 connection.cancel_timed_out_streams(&mut context, &mut EndpointClient(&mut router));
17016 assert!(
17017 connection.core.stream_table.is_empty(),
17018 "premise: the idle guard reaped stream 1"
17019 );
17020
17021 let mut read_on = false;
17022 for _ in 0..256 {
17023 connection.core.readiness.event.insert(Ready::READABLE);
17024 if connection.core.readiness.filter_interest().is_readable() {
17025 connection.readable(&mut context, EndpointClient(&mut router));
17026 }
17027 if !connection.core.output.is_empty() {
17028 read_on = true;
17029 break;
17030 }
17031 std::thread::yield_now();
17032 }
17033 assert!(
17034 read_on,
17035 "a reaped parked stream must not leave the connection unable to read; \
17036 state={:?} expect_read={:?} interest={:?}",
17037 connection.core.state,
17038 connection.core.stream_table.expect_read(),
17039 connection.core.readiness.interest
17040 );
17041 }
17042
17043 /// #1597, backend form: `end_stream` on a `Position::Client` connection
17044 /// (the client gave up, or the backend timeout fired) while the backend
17045 /// is mid-way through a response DATA frame. The RST_STREAM(CANCEL)
17046 /// `end_stream` queues must reach the backend, ahead of the PING ACK the
17047 /// connection queues once it reads on. Before #1604 the RST was written
17048 /// into `zero`, which `handle_header_state` parses from its first byte,
17049 /// so an unflushed RST there was read back as if the backend had sent it,
17050 /// then cleared, and never sent; the output queue shares nothing with the
17051 /// read side any more.
17052 ///
17053 /// The loop drives the connection the way `Mux` does: `writable()` and
17054 /// `readable()` only when the filtered readiness allows them.
17055 ///
17056 /// TO SEE THIS RED: in the `Position::Client` arm of
17057 /// [`ConnectionH2::end_stream`], make the RST's `if` condition start with
17058 /// `false &&`, so the RST is never queued. The test then fails with `the
17059 /// backend must receive exactly RST_STREAM(CANCEL) for stream 1, then the
17060 /// PING ACK`, `left` being the ACK alone. Verified 2026-09-27.
17061 #[test]
17062 fn end_stream_on_a_backend_mid_data_payload_sends_its_rst_before_reading_on() {
17063 use std::io::{Read, Write};
17064
17065 let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 20, 16_384)));
17066 let (socket, mut peer) = connected_socket();
17067 peer.set_nonblocking(true)
17068 .expect("the loopback peer must switch to non-blocking");
17069 let backend = Rc::new(RefCell::new(crate::backends::Backend::new(
17070 "orphan-backend",
17071 "127.0.0.1:2".parse().expect("backend address must parse"),
17072 None,
17073 None,
17074 None,
17075 )));
17076 let mut registry = crate::protocol::mux::BackendRegistry::default();
17077 let mut connection = H2Shell::new(
17078 Ulid::generate(),
17079 socket,
17080 Position::Client(
17081 "orphan-cluster".into(),
17082 registry.id_for(&backend),
17083 BackendStatus::Connected,
17084 ),
17085 &mut PoolBufferSource::new(Rc::downgrade(&pool)),
17086 H2FloodConfig::default(),
17087 H2ConnectionConfig::default(),
17088 Duration::from_secs(30),
17089 None,
17090 Duration::from_secs(30),
17091 Some((H2StreamId::Zero, 9)),
17092 Ready::READABLE | Ready::HUP | Ready::ERROR,
17093 )
17094 .expect("a pool with free buffers must yield an H2 connection");
17095 connection.core.state = H2State::Header;
17096 let mut context = test_context(&pool);
17097 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
17098 let gid = context
17099 .create_stream(Ulid::generate(), 1 << 16)
17100 .expect("test context must create a stream");
17101 context.streams[gid].state = StreamState::Linked(mio::Token(1));
17102 // The backend is answering, so the request's HEADERS reached it: only
17103 // a stream opened on the wire is reset (sozu-proxy/sozu#1631).
17104 context.streams[gid].front.consumed = true;
17105 connection
17106 .core
17107 .stream_table
17108 .register(1, gid, connection.core.now);
17109
17110 // The backend starts a 10-byte response DATA frame and sends 4 bytes.
17111 peer.write_all(&orphan_frame(0, 0, 1, 10, b"abcd"))
17112 .expect("loopback write must complete");
17113 for _ in 0..64 {
17114 connection.core.readiness.event.insert(Ready::READABLE);
17115 connection.readable(&mut context, EndpointClient(&mut router));
17116 if matches!(
17117 connection.core.stream_table.expect_read(),
17118 Some((H2StreamId::Other { .. }, 6))
17119 ) {
17120 break;
17121 }
17122 std::thread::yield_now();
17123 }
17124 assert!(
17125 matches!(
17126 connection.core.stream_table.expect_read(),
17127 Some((H2StreamId::Other { .. }, 6))
17128 ),
17129 "premise: mid response DATA payload with 6 bytes left, got {:?}",
17130 connection.core.stream_table.expect_read()
17131 );
17132
17133 connection.end_stream(gid, &mut context);
17134
17135 // The backend, not yet aware of the RST, finishes the frame and pings.
17136 let mut rest = b"efghij".to_vec();
17137 rest.extend(orphan_ping());
17138 peer.write_all(&rest).expect("loopback write must complete");
17139
17140 let mut received = Vec::new();
17141 let expected_len = 13 + 17;
17142 for _ in 0..256 {
17143 connection
17144 .core
17145 .readiness
17146 .event
17147 .insert(Ready::READABLE | Ready::WRITABLE);
17148 if connection.core.readiness.filter_interest().is_writable() {
17149 connection.writable(&mut context, EndpointClient(&mut router));
17150 }
17151 if connection.core.readiness.filter_interest().is_readable() {
17152 connection.readable(&mut context, EndpointClient(&mut router));
17153 }
17154 let mut buf = [0u8; 256];
17155 if let Ok(n) = peer.read(&mut buf) {
17156 received.extend_from_slice(&buf[..n]);
17157 }
17158 if received.len() >= expected_len
17159 || matches!(connection.core.state, H2State::GoAway | H2State::Error)
17160 {
17161 break;
17162 }
17163 std::thread::yield_now();
17164 }
17165
17166 let mut expected = orphan_frame(3, 0, 1, 4, &(H2Error::Cancel as u32).to_be_bytes());
17167 expected.extend(orphan_frame(6, parser::FLAG_ACK, 0, 8, b"pingpong"));
17168 assert_eq!(
17169 received,
17170 expected,
17171 "the backend must receive exactly RST_STREAM(CANCEL) for stream 1, then the \
17172 PING ACK; state={:?} expect_read={:?}",
17173 connection.core.state,
17174 connection.core.stream_table.expect_read()
17175 );
17176 }
17177
17178 /// What [`backend_with_a_linked_stream`] builds. Callers bind `_pool`
17179 /// first so it is dropped last, after everything holding its buffers.
17180 struct LinkedBackend {
17181 connection: H2Shell<mio::net::TcpStream>,
17182 /// The backend's end of the socket.
17183 peer: std::net::TcpStream,
17184 context: Context<TestListener>,
17185 router: Router,
17186 /// The linked stream's global id.
17187 gid: GlobalStreamId,
17188 _pool: Rc<RefCell<Pool>>,
17189 }
17190
17191 /// A backend (`Position::Client`) connection in `state` over a loopback
17192 /// socket, with `interest`, and one stream linked to it by
17193 /// [`H2Shell::start_stream`]: stream 1, whose HEADERS no write pass has
17194 /// sent.
17195 fn backend_with_a_linked_stream(
17196 state: H2State,
17197 status: BackendStatus,
17198 interest: Ready,
17199 ) -> LinkedBackend {
17200 let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 20, 16_384)));
17201 let (socket, peer) = connected_socket();
17202 let backend = Rc::new(RefCell::new(crate::backends::Backend::new(
17203 "idle-backend",
17204 "127.0.0.1:2".parse().expect("backend address must parse"),
17205 None,
17206 None,
17207 None,
17208 )));
17209 let mut registry = crate::protocol::mux::BackendRegistry::default();
17210 let expect_read = match state {
17211 H2State::ClientPreface | H2State::ClientSettings => None,
17212 _ => Some((H2StreamId::Zero, 9)),
17213 };
17214 let mut connection = H2Shell::new(
17215 Ulid::generate(),
17216 socket,
17217 Position::Client("idle-cluster".into(), registry.id_for(&backend), status),
17218 &mut PoolBufferSource::new(Rc::downgrade(&pool)),
17219 H2FloodConfig::default(),
17220 H2ConnectionConfig::default(),
17221 Duration::from_secs(30),
17222 None,
17223 Duration::from_secs(30),
17224 expect_read,
17225 interest,
17226 )
17227 .expect("a pool with free buffers must yield an H2 connection");
17228 connection.core.state = state;
17229 let mut context = test_context(&pool);
17230 let router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
17231 let gid = context
17232 .create_stream(Ulid::generate(), 1 << 16)
17233 .expect("test context must create a stream");
17234 assert!(
17235 connection.start_stream(gid, &mut context),
17236 "premise: the backend connection accepts the stream"
17237 );
17238 assert_eq!(
17239 connection.core.stream_table.get(1),
17240 Some(gid),
17241 "premise: the stream got wire id 1"
17242 );
17243 LinkedBackend {
17244 connection,
17245 peer,
17246 context,
17247 router,
17248 gid,
17249 _pool: pool,
17250 }
17251 }
17252
17253 /// Drive a backend connection the way `Mux` does — `writable()` and
17254 /// `readable()` only when the filtered readiness allows them — and answer
17255 /// every byte its peer end received.
17256 fn drive_and_read_backend(
17257 connection: &mut H2Shell<mio::net::TcpStream>,
17258 peer: &mut std::net::TcpStream,
17259 context: &mut Context<TestListener>,
17260 router: &mut Router,
17261 ) -> Vec<u8> {
17262 use std::io::Read;
17263
17264 let mut received = Vec::new();
17265 for _ in 0..64 {
17266 connection
17267 .core
17268 .readiness
17269 .event
17270 .insert(Ready::READABLE | Ready::WRITABLE);
17271 if connection.core.readiness.filter_interest().is_writable() {
17272 connection.writable(context, EndpointClient(router));
17273 }
17274 if connection.core.readiness.filter_interest().is_readable() {
17275 connection.readable(context, EndpointClient(router));
17276 }
17277 let mut buf = [0u8; 256];
17278 if let Ok(n) = peer.read(&mut buf) {
17279 received.extend_from_slice(&buf[..n]);
17280 }
17281 std::thread::yield_now();
17282 }
17283 received
17284 }
17285
17286 /// Queue a GET request on `gid`'s request side, closed with END_STREAM,
17287 /// the way a relayed request reaches a backend's write pass.
17288 fn queue_request(context: &mut Context<TestListener>, gid: GlobalStreamId) {
17289 let kawa = &mut context.streams[gid].front;
17290 kawa.detached.status_line = kawa::StatusLine::Request {
17291 version: kawa::Version::V20,
17292 method: kawa::Store::Static(b"GET"),
17293 uri: kawa::Store::Static(b"/"),
17294 authority: kawa::Store::Static(b"example.com"),
17295 path: kawa::Store::Static(b"/"),
17296 };
17297 kawa.push_block(kawa::Block::StatusLine);
17298 kawa.push_block(kawa::Block::Header(kawa::Pair {
17299 key: kawa::Store::Static(b"x-client"),
17300 val: kawa::Store::Static(b"alice"),
17301 }));
17302 kawa.push_block(kawa::Block::Flags(kawa::Flags {
17303 end_body: false,
17304 end_chunk: false,
17305 end_header: true,
17306 end_stream: true,
17307 }));
17308 kawa.parsing_phase = kawa::ParsingPhase::Terminated;
17309 }
17310
17311 /// Frame-level pin of `handle_data_frame`'s bodiless branches: a backend
17312 /// answers `method` with the HEADERS block `head` (no END_STREAM, so the
17313 /// stream stays linked, RFC 9113 §8.1), then DATA carrying `hello`
17314 /// flagged END_STREAM. Returns the frames the backend reads after the
17315 /// DATA, whether the payload was queued on the response, whether the
17316 /// stream is still tracked, and how many bytes the DATA frame left in the
17317 /// response buffer.
17318 fn bodiless_response_with_data(
17319 method: crate::protocol::kawa_h1::parser::Method,
17320 head: &[u8],
17321 ) -> (Vec<PeerFrame>, bool, bool, usize) {
17322 use std::io::Write;
17323
17324 let LinkedBackend {
17325 _pool,
17326 mut connection,
17327 mut peer,
17328 mut context,
17329 mut router,
17330 gid,
17331 } = backend_with_a_linked_stream(
17332 H2State::Header,
17333 BackendStatus::Connected,
17334 Ready::READABLE | Ready::HUP | Ready::ERROR,
17335 );
17336 queue_request(&mut context, gid);
17337 context.streams[gid].context.method = Some(method);
17338 // Open on the client side, as a stream the router linked; `Link`
17339 // carries no frontend token for `reset_stream` to end.
17340 context.streams[gid].state = StreamState::Link;
17341 let request = drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
17342 assert!(
17343 peer_frames(&request)
17344 .expect("whole frames")
17345 .iter()
17346 .any(|(kind, _, id, _)| *kind == 1 && *id == 1),
17347 "premise: the request went out on stream 1, got {request:?}"
17348 );
17349
17350 peer.write_all(&orphan_frame(1, 0x4, 1, head.len() as u32, head))
17351 .expect("loopback write must complete");
17352 drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
17353 let end_before = context.streams[gid].back.storage.end;
17354 peer.write_all(&orphan_frame(0, 0x1, 1, 5, b"hello"))
17355 .expect("loopback write must complete");
17356 let after = drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
17357 let after = peer_frames(&after).expect("whole frames");
17358 assert!(
17359 matches!(connection.core.state, H2State::Header),
17360 "the shared connection stays up, got {:?}",
17361 connection.core.state
17362 );
17363 let back = &context.streams[gid].back;
17364 let buffer = back.storage.buffer();
17365 let queued = back.blocks.iter().any(|block| {
17366 matches!(block, kawa::Block::Chunk(kawa::Chunk { data })
17367 if !data.data(buffer).is_empty())
17368 });
17369 let tracked = connection.core.stream_table.get(1).is_some();
17370 let retained = back.storage.end.saturating_sub(end_before);
17371 (after, queued, tracked, retained)
17372 }
17373
17374 /// RFC 9110 §15.3.5, §15.4.5, RFC 9113 §8.1.1: DATA carrying a payload
17375 /// on a 204 still linked to its backend stream resets that stream with
17376 /// PROTOCOL_ERROR before the payload is queued, without a GOAWAY. RFC 9110
17377 /// §9.3.2: the payload of a response to HEAD is dropped from the stream
17378 /// buffer and never queued, and its END_STREAM still ends the response,
17379 /// with no frame sent. A 200 to GET forwards the payload.
17380 ///
17381 /// TO SEE THIS RED: make `ConnectionH2::backend_response_content` return
17382 /// `BackendResponseContent::Forwarded` for every response, or keep the
17383 /// discarded payload behind `storage.head` in `handle_data_frame`.
17384 #[test]
17385 fn a_204_response_carrying_data_resets_its_backend_stream_and_a_head_response_discards_it() {
17386 use crate::protocol::kawa_h1::parser::Method;
17387
17388 // :status 204 (static index 9).
17389 let (frames, queued, tracked, _) = bodiless_response_with_data(Method::Get, &[0x89]);
17390 assert_eq!(
17391 frames,
17392 vec![(3, 0, 1, vec![0, 0, 0, 1])],
17393 "a 204 carrying DATA gets RST_STREAM(PROTOCOL_ERROR) alone"
17394 );
17395 assert!(!queued, "the payload of a 204 is never queued");
17396 assert!(!tracked, "the reset stream is retired");
17397
17398 // :status 200 (static index 8), to HEAD.
17399 let (frames, queued, _, retained) = bodiless_response_with_data(Method::Head, &[0x88]);
17400 assert!(
17401 !frames
17402 .iter()
17403 .any(|(kind, _, _, _)| *kind == 3 || *kind == 7),
17404 "a response to HEAD carrying DATA is not reset, got {frames:?}"
17405 );
17406 assert!(!queued, "the payload of a response to HEAD is never queued");
17407 assert_eq!(
17408 retained, 0,
17409 "the payload of a response to HEAD leaves no byte in the buffer"
17410 );
17411
17412 let (frames, queued, _, retained) = bodiless_response_with_data(Method::Get, &[0x88]);
17413 assert!(
17414 !frames
17415 .iter()
17416 .any(|(kind, _, _, _)| *kind == 3 || *kind == 7),
17417 "premise: a 200 to GET is not reset, got {frames:?}"
17418 );
17419 assert!(queued, "premise: the payload of a 200 to GET is queued");
17420 assert_eq!(
17421 retained, 5,
17422 "premise: a forwarded payload stays in the buffer"
17423 );
17424 }
17425
17426 /// RFC 9113 §5.1: after the PROTOCOL_ERROR reset of a 204 carrying DATA,
17427 /// the trailer block the backend had already sent on that stream is
17428 /// ignored, in one HEADERS frame or split over a CONTINUATION frame
17429 /// (RFC 9113 §6.10). The shared backend connection gets no GOAWAY and no
17430 /// further RST_STREAM, and stays up.
17431 ///
17432 /// TO SEE THIS RED: in the `H2State::Discard` arm of
17433 /// `ConnectionH2::handle_read` (`lib/src/protocol/mux/h2.rs`), drop the
17434 /// `pending_discarded_block` store: the CONTINUATION is taken for a
17435 /// standalone frame and answered GOAWAY(PROTOCOL_ERROR):
17436 /// `split=true: ... got [(7, 0, 0, [0, 0, 0, 0, 0, 0, 0, 1])]`, verified
17437 /// 2026-10-02. The split row is also red on `2ce07244`
17438 /// (sozu-proxy/sozu#1775 before sozu-proxy/sozu#1784 was merged), where
17439 /// the unsplit row already passes.
17440 #[test]
17441 fn a_trailer_after_the_reset_of_a_204_carrying_data_is_ignored() {
17442 use std::io::Write;
17443
17444 for split in [false, true] {
17445 let LinkedBackend {
17446 _pool,
17447 mut connection,
17448 mut peer,
17449 mut context,
17450 mut router,
17451 gid,
17452 } = backend_with_a_linked_stream(
17453 H2State::Header,
17454 BackendStatus::Connected,
17455 Ready::READABLE | Ready::HUP | Ready::ERROR,
17456 );
17457 queue_request(&mut context, gid);
17458 context.streams[gid].context.method =
17459 Some(crate::protocol::kawa_h1::parser::Method::Get);
17460 context.streams[gid].state = StreamState::Link;
17461 drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
17462
17463 // :status 204 (static index 9), without END_STREAM, then DATA
17464 // without END_STREAM: the backend means to end with a trailer.
17465 peer.write_all(&orphan_frame(1, 0x4, 1, 1, &[0x89]))
17466 .expect("loopback write must complete");
17467 drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
17468 peer.write_all(&orphan_frame(0, 0x0, 1, 5, b"hello"))
17469 .expect("loopback write must complete");
17470 let reset =
17471 drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
17472 let reset = peer_frames(&reset).expect("whole frames");
17473 assert_eq!(
17474 reset,
17475 vec![(3, 0, 1, vec![0, 0, 0, 1])],
17476 "split={split}: premise: a 204 carrying DATA gets RST_STREAM(PROTOCOL_ERROR) alone"
17477 );
17478
17479 // The trailer block `x-trailer: one` (literal without indexing),
17480 // ending the stream.
17481 let mut trailer = vec![0x00, 9];
17482 trailer.extend_from_slice(b"x-trailer");
17483 trailer.extend_from_slice(&[3]);
17484 trailer.extend_from_slice(b"one");
17485 let wire = if split {
17486 let (first, rest) = trailer.split_at(4);
17487 let mut wire = orphan_frame(1, 0x1, 1, first.len() as u32, first);
17488 wire.extend(orphan_frame(9, 0x4, 1, rest.len() as u32, rest));
17489 wire
17490 } else {
17491 orphan_frame(1, 0x4 | 0x1, 1, trailer.len() as u32, &trailer)
17492 };
17493 peer.write_all(&wire).expect("loopback write must complete");
17494 let after =
17495 drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
17496 let after = peer_frames(&after).expect("whole frames");
17497 assert!(
17498 !after.iter().any(|(kind, _, _, _)| *kind == 7 || *kind == 3),
17499 "split={split}: the trailer on the reset stream is ignored: no GOAWAY, no \
17500 RST_STREAM, got {after:?}"
17501 );
17502 assert!(
17503 matches!(connection.core.state, H2State::Header),
17504 "split={split}: the shared connection stays up, got {:?}",
17505 connection.core.state
17506 );
17507 }
17508 }
17509
17510 /// sozu-proxy/sozu#1631, `Header` row: a backend stream ended before any
17511 /// write pass sent its HEADERS is idle on the backend, and a RST_STREAM
17512 /// for it is a connection error there (RFC 9113 §5.1, §6.4) that takes
17513 /// down every stream of the shared connection. It must be forgotten: no
17514 /// frame at all.
17515 ///
17516 /// TO SEE THIS RED: in the `Position::Client` arm of
17517 /// [`ConnectionH2::end_stream`], replace `opened_on_the_wire` in the RST
17518 /// condition with `true`. The backend then reads `RST_STREAM(1, CANCEL)`:
17519 /// `left: [0, 0, 4, 3, 0, 0, 0, 0, 1, 0, 0, 0, 8], right: []`. Red on
17520 /// `d5161919`, verified 2026-09-28.
17521 #[test]
17522 fn a_backend_stream_ended_before_its_headers_left_is_not_reset() {
17523 let LinkedBackend {
17524 _pool,
17525 mut connection,
17526 mut peer,
17527 mut context,
17528 mut router,
17529 gid,
17530 } = backend_with_a_linked_stream(
17531 H2State::Header,
17532 BackendStatus::Connected,
17533 Ready::READABLE | Ready::HUP | Ready::ERROR,
17534 );
17535
17536 connection.end_stream(gid, &mut context);
17537 let received =
17538 drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
17539
17540 assert_eq!(
17541 received,
17542 Vec::<u8>::new(),
17543 "the backend must receive nothing for a stream it never saw"
17544 );
17545 assert!(
17546 connection.core.stream_table.is_empty()
17547 && !connection.core.stream_table.rst_sent_contains(1),
17548 "the stream is retired without a reset"
17549 );
17550 assert!(
17551 matches!(connection.core.state, H2State::Header),
17552 "the shared connection stays up, got {:?}",
17553 connection.core.state
17554 );
17555 }
17556
17557 /// sozu-proxy/sozu#1631, `ClientPreface` row: the backend connection's
17558 /// TCP connect is still in progress when the stream ends. The preface
17559 /// must be the first bytes the backend reads (RFC 9113 §3.4), with no
17560 /// frame for the stream anywhere.
17561 ///
17562 /// TO SEE THIS RED: same edit as
17563 /// `a_backend_stream_ended_before_its_headers_left_is_not_reset`. The
17564 /// backend then reads the RST before `PRI * HTTP/2.0`: `the connection
17565 /// preface must come first`. Red on `d5161919`, verified 2026-09-28.
17566 #[test]
17567 fn a_backend_stream_ended_before_the_preface_leaves_the_preface_first() {
17568 let LinkedBackend {
17569 _pool,
17570 mut connection,
17571 mut peer,
17572 mut context,
17573 mut router,
17574 gid,
17575 } = backend_with_a_linked_stream(
17576 H2State::ClientPreface,
17577 BackendStatus::Connecting(Instant::now()),
17578 Ready::WRITABLE | Ready::HUP | Ready::ERROR,
17579 );
17580
17581 connection.end_stream(gid, &mut context);
17582 let received =
17583 drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
17584
17585 let preface = serializer::H2_PRI.as_bytes();
17586 assert!(
17587 received.starts_with(preface),
17588 "the connection preface must come first, got {received:?}"
17589 );
17590 let frames =
17591 peer_frames(&received[preface.len()..]).expect("whole frames after the preface");
17592 assert!(
17593 frames.iter().all(|(kind, _, id, _)| *id == 0 && *kind != 3),
17594 "only connection-level frames may follow, got {frames:?}"
17595 );
17596 }
17597
17598 /// sozu-proxy/sozu#1631, `ServerSettings` row: the preface is out and the
17599 /// server's SETTINGS are not read yet. Neither ending a stream nor
17600 /// linking one may kill the connection; `start_stream` arms a write, as
17601 /// it must for `Router::plan_connect`, whose pool scan links onto a
17602 /// `Connected` connection whatever its handshake state.
17603 ///
17604 /// TO SEE THIS RED: in [`ConnectionH2::dispatch_writable_state`], move
17605 /// `(H2State::ServerSettings, Position::Client(..))` back into the
17606 /// `Unexpected combination` arm. The connection then force-disconnects:
17607 /// `the shared connection must survive the handshake, got Error`. Red on
17608 /// `d5161919`, verified 2026-09-28.
17609 #[test]
17610 fn a_backend_stream_ended_during_the_handshake_keeps_the_connection() {
17611 let LinkedBackend {
17612 _pool,
17613 mut connection,
17614 mut peer,
17615 mut context,
17616 mut router,
17617 gid,
17618 } = backend_with_a_linked_stream(
17619 H2State::ServerSettings,
17620 BackendStatus::Connected,
17621 Ready::READABLE | Ready::HUP | Ready::ERROR,
17622 );
17623
17624 connection.end_stream(gid, &mut context);
17625 let received =
17626 drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
17627
17628 assert!(
17629 matches!(connection.core.state, H2State::ServerSettings),
17630 "the shared connection must survive the handshake, got {:?}",
17631 connection.core.state
17632 );
17633 assert_eq!(
17634 received,
17635 Vec::<u8>::new(),
17636 "the backend must receive nothing for a stream it never saw"
17637 );
17638 }
17639
17640 /// A backend may answer early, before the whole request reached it
17641 /// (RFC 9113 §8.1). When the stream then ends with the request received
17642 /// whole from the client but its tail still unsent toward the backend,
17643 /// the backend never sees END_STREAM: forgetting the stream without a
17644 /// reset leaves it open (half-closed) on the backend, holding one of the
17645 /// backend's MAX_CONCURRENT_STREAMS slots for this connection's lifetime
17646 /// (RFC 9113 §5.1.2). The stream must be cancelled, exactly once.
17647 ///
17648 /// TO SEE THIS RED: in the `Position::Client` arm of
17649 /// [`ConnectionH2::end_stream`], drop `&& stream.front.is_completed()`
17650 /// from `fully_completed`. The backend then reads no RST_STREAM: `the
17651 /// backend must read RST_STREAM(1, CANCEL) once`, `right` holding that
17652 /// frame and `left` nothing.
17653 #[test]
17654 fn a_backend_stream_answered_before_its_request_was_written_is_cancelled() {
17655 let LinkedBackend {
17656 _pool,
17657 mut connection,
17658 mut peer,
17659 mut context,
17660 mut router,
17661 gid,
17662 } = backend_with_a_linked_stream(
17663 H2State::Header,
17664 BackendStatus::Connected,
17665 Ready::READABLE | Ready::HUP | Ready::ERROR,
17666 );
17667 // A request received whole from the client: its HEADERS, then a
17668 // 4-byte body closed with END_STREAM.
17669 let kawa = &mut context.streams[gid].front;
17670 kawa.detached.status_line = kawa::StatusLine::Request {
17671 version: kawa::Version::V20,
17672 method: kawa::Store::Static(b"POST"),
17673 uri: kawa::Store::Static(b"/"),
17674 authority: kawa::Store::Static(b"example.com"),
17675 path: kawa::Store::Static(b"/"),
17676 };
17677 kawa.push_block(kawa::Block::StatusLine);
17678 kawa.push_block(kawa::Block::Header(kawa::Pair {
17679 key: kawa::Store::Static(b"content-length"),
17680 val: kawa::Store::Static(b"4"),
17681 }));
17682 kawa.push_block(kawa::Block::Flags(kawa::Flags {
17683 end_body: false,
17684 end_chunk: false,
17685 end_header: true,
17686 end_stream: false,
17687 }));
17688 kawa.push_block(kawa::Block::Chunk(kawa::Chunk {
17689 data: kawa::Store::Static(b"body"),
17690 }));
17691 kawa.push_block(kawa::Block::Flags(kawa::Flags {
17692 end_body: true,
17693 end_chunk: false,
17694 end_header: false,
17695 end_stream: true,
17696 }));
17697 kawa.body_size = kawa::BodySize::Length(4);
17698 kawa.parsing_phase = kawa::ParsingPhase::Terminated;
17699 // The backend grants the stream no send window: its HEADERS leave,
17700 // its body waits.
17701 context.streams[gid].back_window = 0;
17702 let sent = drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
17703 let frames = peer_frames(&sent).expect("whole frames");
17704 assert!(
17705 frames
17706 .iter()
17707 .any(|(kind, flags, id, _)| *kind == 1 && *id == 1 && flags & 0x1 == 0)
17708 && !frames.iter().any(|(kind, _, _, _)| *kind == 0),
17709 "premise: the HEADERS left without END_STREAM and no DATA, got {frames:?}"
17710 );
17711 let stream = &context.streams[gid];
17712 assert!(
17713 stream.front.consumed && stream.front.is_terminated() && !stream.front.is_completed(),
17714 "premise: the request was received whole but not written whole"
17715 );
17716 // The backend's complete response arrived: its END_STREAM was read.
17717 context.streams[gid].back_received_end_of_stream = true;
17718
17719 connection.end_stream(gid, &mut context);
17720 // Ending it again must not reset it a second time.
17721 connection.end_stream(gid, &mut context);
17722 let received =
17723 drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
17724
17725 assert_eq!(
17726 received,
17727 orphan_frame(3, 0, 1, 4, &(H2Error::Cancel as u32).to_be_bytes()),
17728 "the backend must read RST_STREAM(1, CANCEL) once"
17729 );
17730 assert!(
17731 connection.core.stream_table.is_empty(),
17732 "the stream is retired from the backend connection"
17733 );
17734 assert!(
17735 matches!(connection.core.state, H2State::Header),
17736 "the shared connection stays up, got {:?}",
17737 connection.core.state
17738 );
17739 }
17740
17741 /// The liveness half of the `ServerSettings` fix: the write readiness
17742 /// withdrawn while the server's SETTINGS are awaited comes back with
17743 /// them, and a request linked during the handshake goes out after the
17744 /// SETTINGS ACK.
17745 ///
17746 /// TO SEE THIS RED: in [`ConnectionH2::dispatch_writable_state`], move
17747 /// `(H2State::ServerSettings, Position::Client(..))` back into the
17748 /// `Unexpected combination` arm: the connection force-disconnects before
17749 /// the SETTINGS arrive, `premise: nothing is written before the server
17750 /// SETTINGS, got [] in Error`. Red on `d5161919`, verified 2026-09-28.
17751 #[test]
17752 fn a_request_linked_during_the_handshake_goes_out_after_the_settings() {
17753 use std::io::Write;
17754
17755 let LinkedBackend {
17756 _pool,
17757 mut connection,
17758 mut peer,
17759 mut context,
17760 mut router,
17761 gid,
17762 } = backend_with_a_linked_stream(
17763 H2State::ServerSettings,
17764 BackendStatus::Connected,
17765 Ready::READABLE | Ready::HUP | Ready::ERROR,
17766 );
17767 queue_request(&mut context, gid);
17768 let before = drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
17769 assert!(
17770 before.is_empty() && matches!(connection.core.state, H2State::ServerSettings),
17771 "premise: nothing is written before the server SETTINGS, got {before:?} in {:?}",
17772 connection.core.state
17773 );
17774
17775 peer.write_all(&orphan_frame(4, 0, 0, 0, b""))
17776 .expect("loopback write must complete");
17777 let after = drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
17778
17779 let frames = peer_frames(&after).expect("whole frames");
17780 assert!(
17781 frames
17782 .iter()
17783 .any(|(kind, flags, id, _)| *kind == 4 && *flags == parser::FLAG_ACK && *id == 0),
17784 "the server SETTINGS are acknowledged, got {frames:?}"
17785 );
17786 assert!(
17787 frames.iter().any(|(kind, _, id, _)| *kind == 1 && *id == 1),
17788 "the request linked during the handshake must go out, got {frames:?}"
17789 );
17790 }
17791
17792 /// A backend stream 1 whose response is framed by `content-length: 3`,
17793 /// its request sent and its response carrying 5 bytes of DATA, which
17794 /// makes Sōzu reset it with PROTOCOL_ERROR (RFC 9113 §8.1.1). Returns the
17795 /// fixture, ready for the frames the backend sends after the reset.
17796 fn backend_stream_reset_for_its_content_length() -> LinkedBackend {
17797 use std::io::Write;
17798
17799 let mut linked = backend_with_a_linked_stream(
17800 H2State::Header,
17801 BackendStatus::Connected,
17802 Ready::READABLE | Ready::HUP | Ready::ERROR,
17803 );
17804 let LinkedBackend {
17805 connection,
17806 peer,
17807 context,
17808 router,
17809 gid,
17810 ..
17811 } = &mut linked;
17812 queue_request(context, *gid);
17813 // Open on the client side, as a stream the router linked; `Link`
17814 // carries no frontend token for `reset_stream` to end.
17815 context.streams[*gid].state = StreamState::Link;
17816 let request = drive_and_read_backend(connection, peer, context, router);
17817 assert!(
17818 peer_frames(&request)
17819 .expect("whole frames")
17820 .iter()
17821 .any(|(kind, _, id, _)| *kind == 1 && *id == 1),
17822 "premise: the request went out on stream 1, got {request:?}"
17823 );
17824 // :status 200 (static index 8), then `content-length: 3` as a
17825 // literal without indexing.
17826 let mut head = vec![0x88, 0x00, 14];
17827 head.extend_from_slice(b"content-length");
17828 head.extend_from_slice(&[1, b'3']);
17829 let mut wire = orphan_frame(1, 0x4, 1, head.len() as u32, &head);
17830 wire.extend(orphan_frame(0, 0, 1, 5, b"hello"));
17831 peer.write_all(&wire).expect("loopback write must complete");
17832 let reset = drive_and_read_backend(connection, peer, context, router);
17833 let reset = peer_frames(&reset).expect("whole frames");
17834 assert!(
17835 reset
17836 .iter()
17837 .any(|(kind, _, id, payload)| *kind == 3 && *id == 1 && payload == &[0, 0, 0, 1]),
17838 "premise: stream 1 is reset with PROTOCOL_ERROR, got {reset:?}"
17839 );
17840 linked
17841 }
17842
17843 /// The trailer block adding `x-dyn: one` to the HPACK dynamic table
17844 /// (literal with incremental indexing) on stream 1, in one HEADERS frame
17845 /// or, with `split`, a HEADERS frame without END_HEADERS and a
17846 /// CONTINUATION frame (RFC 9113 §6.10).
17847 fn dynamic_trailer_block(split: bool) -> Vec<u8> {
17848 let mut trailer = vec![0x40, 5];
17849 trailer.extend_from_slice(b"x-dyn");
17850 trailer.extend_from_slice(&[3]);
17851 trailer.extend_from_slice(b"one");
17852 if split {
17853 let (first, rest) = trailer.split_at(4);
17854 let mut wire = orphan_frame(1, 0x1, 1, first.len() as u32, first);
17855 wire.extend(orphan_frame(9, 0x4, 1, rest.len() as u32, rest));
17856 wire
17857 } else {
17858 orphan_frame(1, 0x4 | 0x1, 1, trailer.len() as u32, &trailer)
17859 }
17860 }
17861
17862 /// RFC 9113 §5.1: after sozu resets a backend stream, the backend may
17863 /// still send frames it queued before reading the RST_STREAM, and sozu
17864 /// MUST ignore them. Here the backend sends a trailer block, in one
17865 /// HEADERS frame or split over a CONTINUATION frame, and another DATA
17866 /// frame on the reset stream. The trailer block adds `x-dyn: one` to the
17867 /// HPACK dynamic table, and the response on stream 3 refers to that
17868 /// entry, so it parses only if the ignored block was decoded whole
17869 /// (§4.3). What the backend had in flight counts no glitch.
17870 ///
17871 /// TO SEE THIS RED: in `ConnectionH2::handle_header_state`, drop the
17872 /// `was_reset_locally` branch: the trailer HEADERS is answered
17873 /// GOAWAY(STREAM_CLOSED); or, in the `H2State::Discard` arm,
17874 /// drop the `pending_discarded_block` store: the CONTINUATION is
17875 /// answered GOAWAY(PROTOCOL_ERROR) as standalone.
17876 #[test]
17877 fn frames_on_a_backend_stream_sozu_reset_are_ignored() {
17878 use std::io::Write;
17879
17880 for split in [true, false] {
17881 let LinkedBackend {
17882 _pool,
17883 mut connection,
17884 mut peer,
17885 mut context,
17886 mut router,
17887 ..
17888 } = backend_stream_reset_for_its_content_length();
17889 let glitches = connection.core.flood_detector.glitch_count();
17890
17891 let mut wire = dynamic_trailer_block(split);
17892 wire.extend(orphan_frame(0, 0x1, 1, 2, b"zz"));
17893 peer.write_all(&wire).expect("loopback write must complete");
17894 let after =
17895 drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
17896 let after = peer_frames(&after).expect("whole frames");
17897 assert!(
17898 !after.iter().any(|(kind, _, _, _)| *kind == 7 || *kind == 3),
17899 "split={split}: frames on the reset stream are ignored: no GOAWAY, no \
17900 RST_STREAM, got {after:?}"
17901 );
17902 assert!(
17903 matches!(connection.core.state, H2State::Header),
17904 "split={split}: the shared connection stays up, got {:?}",
17905 connection.core.state
17906 );
17907 assert_eq!(
17908 connection.core.flood_detector.glitch_count(),
17909 glitches,
17910 "split={split}: what the backend had in flight counts no glitch"
17911 );
17912
17913 // A second stream, answered with `:status 200` and the dynamic
17914 // entry the ignored trailer block added (index 62).
17915 let second = context
17916 .create_stream(Ulid::generate(), 1 << 16)
17917 .expect("test context must create a stream");
17918 assert!(
17919 connection.start_stream(second, &mut context),
17920 "the backend connection accepts a second stream"
17921 );
17922 queue_request(&mut context, second);
17923 context.streams[second].state = StreamState::Link;
17924 let request =
17925 drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
17926 assert!(
17927 peer_frames(&request)
17928 .expect("whole frames")
17929 .iter()
17930 .any(|(kind, _, id, _)| *kind == 1 && *id == 3),
17931 "premise: the second request went out on stream 3, got {request:?}"
17932 );
17933 peer.write_all(&orphan_frame(1, 0x4 | 0x1, 3, 2, &[0x88, 0xbe]))
17934 .expect("loopback write must complete");
17935 let last =
17936 drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
17937 let last = peer_frames(&last).expect("whole frames");
17938 assert!(
17939 !last.iter().any(|(kind, _, _, _)| *kind == 7),
17940 "split={split}: the second response decodes against the same dynamic table, \
17941 got {last:?}"
17942 );
17943 let buffer = context.streams[second].back.storage.buffer();
17944 let has_dynamic_field = context.streams[second].back.blocks.iter().any(|block| {
17945 matches!(block, kawa::Block::Header(kawa::Pair { key, val })
17946 if key.data(buffer) == b"x-dyn" && val.data(buffer) == b"one")
17947 });
17948 assert!(
17949 has_dynamic_field,
17950 "split={split}: the second response carries the field of the dynamic entry"
17951 );
17952 }
17953 }
17954
17955 /// DATA a backend sends on a stream Sōzu reset counts no glitch while it
17956 /// fits the stream's receive window, the 65 535 bytes Sōzu advertised
17957 /// and keeps crediting back: the backend may have had that much in
17958 /// flight when it read the reset (RFC 9113 §6.9). DATA beyond it counts
17959 /// one glitch per frame.
17960 ///
17961 /// TO SEE THIS RED: in `ConnectionH2::handle_header_state`, count a glitch for
17962 /// every frame ignored on a reset stream: `in-flight DATA counts no
17963 /// glitch`.
17964 #[test]
17965 fn data_on_a_reset_backend_stream_counts_a_glitch_beyond_its_window_only() {
17966 use std::io::Write;
17967
17968 let LinkedBackend {
17969 _pool,
17970 mut connection,
17971 mut peer,
17972 mut context,
17973 mut router,
17974 ..
17975 } = backend_stream_reset_for_its_content_length();
17976 let glitches = connection.core.flood_detector.glitch_count();
17977 // 5 bytes already arrived before the reset; four 16 000-byte frames
17978 // still fit in the 65 535-byte window.
17979 let chunk = vec![b'd'; 16_000];
17980 let mut wire = Vec::new();
17981 for _ in 0..4 {
17982 wire.extend(orphan_frame(0, 0, 1, chunk.len() as u32, &chunk));
17983 }
17984 peer.write_all(&wire).expect("loopback write must complete");
17985 drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
17986 assert_eq!(
17987 connection.core.flood_detector.glitch_count(),
17988 glitches,
17989 "in-flight DATA counts no glitch"
17990 );
17991
17992 peer.write_all(&orphan_frame(0, 0, 1, chunk.len() as u32, &chunk))
17993 .expect("loopback write must complete");
17994 let after = drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
17995 assert_eq!(
17996 connection.core.flood_detector.glitch_count(),
17997 glitches + 1,
17998 "DATA beyond the stream's window counts a glitch"
17999 );
18000 assert!(
18001 !peer_frames(&after)
18002 .expect("whole frames")
18003 .iter()
18004 .any(|(kind, _, _, _)| *kind == 7 || *kind == 3),
18005 "it is still ignored, not answered"
18006 );
18007 }
18008
18009 /// Header blocks a backend sends on a stream Sōzu reset count no glitch
18010 /// for the first two, a trailer section or an interim and a final
18011 /// response it may have had in flight (RFC 9113 §8.1), and one glitch
18012 /// each beyond: a peer repeating blocks on a reset stream trips the
18013 /// glitch limit and the connection ends with GOAWAY(ENHANCE_YOUR_CALM),
18014 /// instead of having every block HPACK-decoded for free.
18015 ///
18016 /// TO SEE THIS RED: in `ConnectionH2::handle_header_state`, count every header
18017 /// block on a reset stream as in flight: no GOAWAY ever comes.
18018 #[test]
18019 fn header_blocks_on_a_reset_backend_stream_beyond_two_count_glitches() {
18020 use std::io::Write;
18021
18022 let LinkedBackend {
18023 _pool,
18024 mut connection,
18025 mut peer,
18026 mut context,
18027 mut router,
18028 ..
18029 } = backend_stream_reset_for_its_content_length();
18030 // Each small write must reach the connection before the next drive,
18031 // not wait behind Nagle's algorithm for the previous one's ACK.
18032 peer.set_nodelay(true).expect("TCP_NODELAY must apply");
18033 // An explicit glitch limit, so the flood below trips it whatever the
18034 // default (`DEFAULT_MAX_GLITCH_COUNT`) is.
18035 connection.core.flood_detector = h2_flood_detector::H2FloodDetector::new(
18036 H2FloodConfig::from_optional(
18037 None,
18038 None,
18039 None,
18040 None,
18041 None,
18042 None,
18043 Some(100),
18044 None,
18045 None,
18046 None,
18047 None,
18048 None,
18049 None,
18050 None,
18051 ),
18052 connection.core.now,
18053 );
18054 let glitches = connection.core.flood_detector.glitch_count();
18055 // `:status 200`, alone, in one HEADERS frame ending the block.
18056 let block = || orphan_frame(1, 0x4, 1, 1, &[0x88]);
18057
18058 let mut wire = block();
18059 wire.extend(block());
18060 peer.write_all(&wire).expect("loopback write must complete");
18061 drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
18062 assert_eq!(
18063 connection.core.flood_detector.glitch_count(),
18064 glitches,
18065 "two header blocks may be in flight"
18066 );
18067
18068 peer.write_all(&block())
18069 .expect("loopback write must complete");
18070 drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
18071 assert_eq!(
18072 connection.core.flood_detector.glitch_count(),
18073 glitches + 1,
18074 "a third header block counts a glitch"
18075 );
18076
18077 let mut received = Vec::new();
18078 for _ in 0..20 {
18079 let mut wire = Vec::new();
18080 for _ in 0..100 {
18081 wire.extend(block());
18082 }
18083 if peer.write_all(&wire).is_err() {
18084 break;
18085 }
18086 received.extend(drive_and_read_backend(
18087 &mut connection,
18088 &mut peer,
18089 &mut context,
18090 &mut router,
18091 ));
18092 if peer_frames(&received)
18093 .map(|frames| frames.iter().any(|(kind, _, _, _)| *kind == 7))
18094 .unwrap_or(false)
18095 {
18096 break;
18097 }
18098 }
18099 let frames = peer_frames(&received).expect("whole frames");
18100 assert!(
18101 frames.iter().any(|(kind, _, _, payload)| *kind == 7
18102 && payload.get(4..8) == Some(&[0, 0, 0, 0xb][..])),
18103 "repeated header blocks end the connection with GOAWAY(ENHANCE_YOUR_CALM), got {frames:?}"
18104 );
18105 }
18106
18107 /// Empty DATA frames without END_STREAM on a stream Sōzu reset count
18108 /// toward the empty-DATA flood limit as on a live stream
18109 /// (CVE-2019-9518): they carry no payload to charge against the stream's
18110 /// window, so without it a peer could send them for free. A flood of
18111 /// them ends the connection with GOAWAY(ENHANCE_YOUR_CALM).
18112 ///
18113 /// TO SEE THIS RED: in `ConnectionH2::handle_header_state`, drop the
18114 /// `record_empty_data_frame` call of the `was_reset_locally` branch: no
18115 /// GOAWAY ever comes.
18116 #[test]
18117 fn empty_data_frames_on_a_reset_backend_stream_count_toward_the_flood_limit() {
18118 use std::io::Write;
18119
18120 let LinkedBackend {
18121 _pool,
18122 mut connection,
18123 mut peer,
18124 mut context,
18125 mut router,
18126 ..
18127 } = backend_stream_reset_for_its_content_length();
18128 peer.set_nodelay(true).expect("TCP_NODELAY must apply");
18129 // An explicit empty-DATA limit, so the flood below trips it whatever the
18130 // default (`DEFAULT_MAX_EMPTY_DATA_PER_WINDOW`) is. The glitch limit
18131 // stays the default: the GOAWAY must come from the empty-DATA count.
18132 connection.core.flood_detector = h2_flood_detector::H2FloodDetector::new(
18133 H2FloodConfig::from_optional(
18134 None,
18135 None,
18136 None,
18137 Some(100),
18138 None,
18139 None,
18140 None,
18141 None,
18142 None,
18143 None,
18144 None,
18145 None,
18146 None,
18147 None,
18148 ),
18149 connection.core.now,
18150 );
18151 let mut received = Vec::new();
18152 for _ in 0..50 {
18153 let mut wire = Vec::new();
18154 for _ in 0..100 {
18155 wire.extend(orphan_frame(0, 0, 1, 0, b""));
18156 }
18157 if peer.write_all(&wire).is_err() {
18158 break;
18159 }
18160 received.extend(drive_and_read_backend(
18161 &mut connection,
18162 &mut peer,
18163 &mut context,
18164 &mut router,
18165 ));
18166 if peer_frames(&received)
18167 .map(|frames| frames.iter().any(|(kind, _, _, _)| *kind == 7))
18168 .unwrap_or(false)
18169 {
18170 break;
18171 }
18172 }
18173 let frames = peer_frames(&received).expect("whole frames");
18174 assert!(
18175 frames.iter().any(|(kind, _, _, payload)| *kind == 7
18176 && payload.get(4..8) == Some(&[0, 0, 0, 0xb][..])),
18177 "a flood of empty DATA frames ends the connection with \
18178 GOAWAY(ENHANCE_YOUR_CALM), got {frames:?}"
18179 );
18180 }
18181
18182 /// A backend stream Sōzu reset from its write pass, because its request
18183 /// failed after its head left, stays tracked until its end: DATA the
18184 /// backend then sends on it is ignored (RFC 9113 §5.1), never queued for
18185 /// the client nor counted against the response.
18186 ///
18187 /// TO SEE THIS RED: in `ConnectionH2::handle_header_state`, drop the
18188 /// `was_reset_locally` branch: the DATA reaches the stream and is counted
18189 /// in `back_data_received`.
18190 #[test]
18191 fn data_on_a_tracked_backend_stream_sozu_reset_is_ignored() {
18192 use std::io::Write;
18193
18194 let LinkedBackend {
18195 _pool,
18196 mut connection,
18197 mut peer,
18198 mut context,
18199 mut router,
18200 gid,
18201 } = backend_with_a_linked_stream(
18202 H2State::Header,
18203 BackendStatus::Connected,
18204 Ready::READABLE | Ready::HUP | Ready::ERROR,
18205 );
18206 // A request whose body is still to come: its head leaves, the
18207 // stream stays open on the request side.
18208 queue_request(&mut context, gid);
18209 let front = &mut context.streams[gid].front;
18210 if let Some(kawa::Block::Flags(flags)) = front.blocks.back_mut() {
18211 flags.end_stream = false;
18212 }
18213 front.body_size = kawa::BodySize::Chunked;
18214 front.parsing_phase = kawa::ParsingPhase::Chunks { first: true };
18215 context.streams[gid].state = StreamState::Link;
18216 drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
18217 // The request side then fails: the write pass resets the stream and
18218 // keeps it tracked.
18219 context.streams[gid]
18220 .front
18221 .parsing_phase
18222 .error("request failed".into());
18223 connection.core.readiness.arm_writable();
18224 let reset = drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
18225 assert!(
18226 peer_frames(&reset)
18227 .expect("whole frames")
18228 .iter()
18229 .any(|(kind, _, id, _)| *kind == 3 && *id == 1),
18230 "premise: the write pass resets stream 1, got {reset:?}"
18231 );
18232 assert!(
18233 connection.core.stream_table.get(1).is_some()
18234 && connection.core.stream_table.rst_sent_contains(1),
18235 "premise: the reset stream is still tracked"
18236 );
18237
18238 peer.write_all(&orphan_frame(0, 0, 1, 5, b"hello"))
18239 .expect("loopback write must complete");
18240 let after = drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
18241 assert_eq!(
18242 context.streams[gid].back_data_received, 0,
18243 "the DATA is not counted against the response"
18244 );
18245 assert!(
18246 !context.streams[gid]
18247 .back
18248 .blocks
18249 .iter()
18250 .any(|block| matches!(block, kawa::Block::Chunk(_))),
18251 "the DATA is not queued for the client"
18252 );
18253 assert!(
18254 !peer_frames(&after)
18255 .expect("whole frames")
18256 .iter()
18257 .any(|(kind, _, _, _)| *kind == 7),
18258 "the connection stays up"
18259 );
18260 }
18261
18262 /// The client-facing side of RFC 9113 §5.1: a request framed by
18263 /// `content-length: 3` whose DATA carries 5 bytes is reset by Sōzu
18264 /// (§8.1.1); the DATA and the trailer block the client had already sent
18265 /// on that stream are ignored, and the connection stays up.
18266 ///
18267 /// TO SEE THIS RED: in `ConnectionH2::handle_header_state`, drop the
18268 /// `was_reset_locally` branch: the trailer HEADERS is answered
18269 /// GOAWAY(STREAM_CLOSED).
18270 #[test]
18271 fn frames_on_a_client_stream_sozu_reset_are_ignored() {
18272 use std::io::Write;
18273
18274 let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 20, 16_384)));
18275 let (mut connection, mut peer) = test_h2_connection(&pool, None);
18276 let mut context = test_context(&pool);
18277 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
18278 connection.core.state = H2State::Header;
18279 connection
18280 .core
18281 .stream_table
18282 .set_expect_read(Some((H2StreamId::Zero, 9)));
18283 let block = crate::protocol::mux::hpack::Encoder::new().encode([
18284 (&b":method"[..], &b"POST"[..]),
18285 (&b":scheme"[..], &b"https"[..]),
18286 (&b":authority"[..], &b"example.com"[..]),
18287 (&b":path"[..], &b"/upload"[..]),
18288 (&b"content-length"[..], &b"3"[..]),
18289 ]);
18290 let mut wire = orphan_frame(1, parser::FLAG_END_HEADERS, 1, block.len() as u32, &block);
18291 wire.extend(orphan_frame(0, 0, 1, 5, b"hello"));
18292 peer.write_all(&wire).expect("loopback write must complete");
18293 let reset = drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
18294 let reset = peer_frames(&reset).expect("whole frames");
18295 assert!(
18296 reset
18297 .iter()
18298 .any(|(kind, _, id, payload)| *kind == 3 && *id == 1 && payload == &[0, 0, 0, 1]),
18299 "premise: stream 1 is reset with PROTOCOL_ERROR, got {reset:?}"
18300 );
18301
18302 let mut wire = orphan_frame(0, 0, 1, 2, b"zz");
18303 wire.extend(dynamic_trailer_block(false));
18304 peer.write_all(&wire).expect("loopback write must complete");
18305 let after = drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
18306 let after = peer_frames(&after).expect("whole frames");
18307 assert!(
18308 !after.iter().any(|(kind, _, _, _)| *kind == 7 || *kind == 3),
18309 "frames on the reset stream are ignored: no GOAWAY, no RST_STREAM, got {after:?}"
18310 );
18311 assert!(
18312 matches!(connection.core.state, H2State::Header),
18313 "the client connection stays up, got {:?}",
18314 connection.core.state
18315 );
18316 }
18317
18318 /// A stream Sōzu refuses at its HEADERS frame (here because
18319 /// SETTINGS_MAX_CONCURRENT_STREAMS is reached) is answered
18320 /// RST_STREAM(REFUSED_STREAM) and its field block discarded, and the
18321 /// CONTINUATION frame completing that block (RFC 9113 §6.10) is
18322 /// discarded with it, not taken for a standalone CONTINUATION.
18323 ///
18324 /// TO SEE THIS RED: in the `H2State::Discard` arm of
18325 /// [`ConnectionH2::handle_read`], drop the `pending_discarded_block`
18326 /// store: the CONTINUATION is answered GOAWAY(PROTOCOL_ERROR).
18327 #[test]
18328 fn a_continuation_of_a_refused_header_block_is_discarded_with_it() {
18329 use std::io::Write;
18330
18331 let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 20, 16_384)));
18332 let (mut connection, mut peer) = test_h2_connection(&pool, None);
18333 let mut context = test_context(&pool);
18334 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
18335 connection.core.state = H2State::Header;
18336 connection
18337 .core
18338 .stream_table
18339 .set_expect_read(Some((H2StreamId::Zero, 9)));
18340 connection
18341 .core
18342 .local_settings
18343 .settings_max_concurrent_streams = 0;
18344 let block = crate::protocol::mux::hpack::Encoder::new().encode([
18345 (&b":method"[..], &b"GET"[..]),
18346 (&b":scheme"[..], &b"https"[..]),
18347 (&b":authority"[..], &b"example.com"[..]),
18348 (&b":path"[..], &b"/"[..]),
18349 ]);
18350 let (first, rest) = block.split_at(block.len() / 2);
18351 let mut wire = orphan_frame(1, parser::FLAG_END_STREAM, 1, first.len() as u32, first);
18352 wire.extend(orphan_frame(
18353 9,
18354 parser::FLAG_END_HEADERS,
18355 1,
18356 rest.len() as u32,
18357 rest,
18358 ));
18359 peer.write_all(&wire).expect("loopback write must complete");
18360 let answer = drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
18361 let answer = peer_frames(&answer).expect("whole frames");
18362 assert!(
18363 answer
18364 .iter()
18365 .any(|(kind, _, id, payload)| *kind == 3 && *id == 1 && payload == &[0, 0, 0, 7]),
18366 "the stream is refused, got {answer:?}"
18367 );
18368 assert!(
18369 !answer.iter().any(|(kind, _, _, _)| *kind == 7),
18370 "the CONTINUATION is discarded with its block, got {answer:?}"
18371 );
18372 assert!(
18373 matches!(connection.core.state, H2State::Header),
18374 "the client connection stays up, got {:?}",
18375 connection.core.state
18376 );
18377 }
18378
18379 /// A refused header block is held to `max_header_list_size` like an
18380 /// accepted one, its first fragment included: a 16 000-byte HEADERS frame
18381 /// and four 12 500-byte CONTINUATION frames exceed the 65 536-byte limit
18382 /// only with that first fragment, and end the connection with
18383 /// GOAWAY(ENHANCE_YOUR_CALM) (CVE-2024-27316).
18384 ///
18385 /// TO SEE THIS RED: in [`ConnectionH2::discard_field_block`], drop the
18386 /// `begin_header_block_if_new` call: the block is read to its end, and
18387 /// the test fails once its 30-second deadline passes without a GOAWAY.
18388 #[test]
18389 fn a_refused_header_block_counts_its_first_fragment_toward_its_size() {
18390 use std::io::Write;
18391
18392 let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 20, 16_384)));
18393 let (mut connection, mut peer) = test_h2_connection(&pool, None);
18394 let mut context = test_context(&pool);
18395 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
18396 connection.core.state = H2State::Header;
18397 connection
18398 .core
18399 .stream_table
18400 .set_expect_read(Some((H2StreamId::Zero, 9)));
18401 connection
18402 .core
18403 .local_settings
18404 .settings_max_concurrent_streams = 0;
18405 let first = vec![0x88; 16_000];
18406 let mut wire = orphan_frame(1, 0, 1, first.len() as u32, &first);
18407 let rest = vec![0x88; 12_500];
18408 for _ in 0..4 {
18409 wire.extend(orphan_frame(9, 0, 1, rest.len() as u32, &rest));
18410 }
18411 // The peer cannot write all of it before the connection reads, so
18412 // write it from a thread while the connection is driven.
18413 let mut writer = peer.try_clone().expect("the peer socket must clone");
18414 let sender = std::thread::spawn(move || writer.write_all(&wire));
18415 let enhance_your_calm = |frames: &[PeerFrame]| {
18416 frames.iter().any(|(kind, _, _, payload)| {
18417 *kind == 7 && payload.get(4..8) == Some(&[0, 0, 0, 0xb][..])
18418 })
18419 };
18420 // Drive until the GOAWAY reaches the peer, not for a fixed number of
18421 // passes: on a loaded host the writer thread can be descheduled for
18422 // all of them, and the GOAWAY the read of the last fragment queues is
18423 // only flushed by the next pass's `writable()`.
18424 let deadline = Instant::now() + Duration::from_secs(30);
18425 let mut received = Vec::new();
18426 while Instant::now() < deadline {
18427 received.extend(drive_and_read_backend(
18428 &mut connection,
18429 &mut peer,
18430 &mut context,
18431 &mut router,
18432 ));
18433 if peer_frames(&received).is_ok_and(|frames| enhance_your_calm(&frames)) {
18434 break;
18435 }
18436 }
18437 sender
18438 .join()
18439 .expect("the writer thread must not panic")
18440 .expect("the peer must write the whole block");
18441 let frames = peer_frames(&received).expect("whole frames");
18442 assert!(
18443 enhance_your_calm(&frames),
18444 "the oversized refused block ends the connection with GOAWAY(ENHANCE_YOUR_CALM), \
18445 got {frames:?}"
18446 );
18447 }
18448
18449 /// sozu-proxy/sozu#1631, reaper form: `cancel_timed_out_streams` has the
18450 /// same shape as `end_stream` for a backend stream reaped before its
18451 /// HEADERS left, and must forget it the same way.
18452 ///
18453 /// TO SEE THIS RED: in [`ConnectionH2::cancel_timed_out_streams`], make
18454 /// `never_opened` `false`. The stream is then reset: `a stream reaped
18455 /// before its HEADERS left must not be reset`. Red on `d5161919`,
18456 /// verified 2026-09-28.
18457 #[test]
18458 fn a_backend_stream_reaped_before_its_headers_left_is_not_reset() {
18459 let LinkedBackend {
18460 _pool,
18461 mut connection,
18462 mut peer,
18463 mut context,
18464 mut router,
18465 gid: _,
18466 } = backend_with_a_linked_stream(
18467 H2State::Header,
18468 BackendStatus::Connected,
18469 Ready::READABLE | Ready::HUP | Ready::ERROR,
18470 );
18471 connection.core.readiness.interest.remove(Ready::WRITABLE);
18472
18473 context.now += Duration::from_secs(31);
18474 connection.cancel_timed_out_streams(&mut context, &mut EndpointClient(&mut router));
18475 assert!(
18476 connection.core.stream_table.is_empty(),
18477 "premise: the idle guard reaped stream 1"
18478 );
18479 assert!(
18480 connection.core.control_tx.pending().is_empty()
18481 && !connection.core.readiness.interest.is_writable()
18482 && connection.core.output.is_empty(),
18483 "a stream reaped before its HEADERS left must not be reset"
18484 );
18485 let received =
18486 drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
18487 assert_eq!(
18488 received,
18489 Vec::<u8>::new(),
18490 "and nothing reaches the backend"
18491 );
18492 }
18493
18494 /// sozu-proxy/sozu#1631, frontend-error form (found by the adversarial
18495 /// review of the first fix): the H1 frontend rejects a malformed request
18496 /// body and answers 400 itself, which leaves the request kawa in `Error`
18497 /// while its backend stream is still registered and its HEADERS never
18498 /// left. The next write pass used to hand it to the converter, whose
18499 /// `initialize` queues a RST_STREAM for that idle stream.
18500 ///
18501 /// TO SEE THIS RED: in [`ConnectionH2::poll_write_target`]'s
18502 /// `H2WritePhase::Prepare` arm, make `never_opened_error` `false`. The
18503 /// backend then reads `RST_STREAM(1)`: `no frame may name stream 1`.
18504 /// Red on `6eef0ba1`, verified 2026-09-28.
18505 #[test]
18506 fn a_backend_stream_whose_request_failed_before_its_headers_left_is_not_reset() {
18507 let LinkedBackend {
18508 _pool,
18509 mut connection,
18510 mut peer,
18511 mut context,
18512 mut router,
18513 gid,
18514 } = backend_with_a_linked_stream(
18515 H2State::Header,
18516 BackendStatus::Connected,
18517 Ready::READABLE | Ready::HUP | Ready::ERROR,
18518 );
18519 queue_request(&mut context, gid);
18520 // What `kawa::h1::parse` leaves on a malformed chunked body.
18521 context.streams[gid]
18522 .front
18523 .parsing_phase
18524 .error(kawa::ParsingErrorKind::Processing {
18525 message: "malformed chunk",
18526 });
18527 assert!(
18528 context.streams[gid].front.is_error() && !context.streams[gid].front.consumed,
18529 "premise: the request failed before any byte left"
18530 );
18531
18532 let received =
18533 drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
18534
18535 let frames = peer_frames(&received).expect("whole frames");
18536 assert!(
18537 frames.iter().all(|(_, _, id, _)| *id != 1),
18538 "no frame may name stream 1, which the backend never saw: {frames:?}"
18539 );
18540 assert!(
18541 matches!(connection.core.state, H2State::Header),
18542 "the shared connection stays up, got {:?}",
18543 connection.core.state
18544 );
18545 }
18546
18547 /// sozu-proxy/sozu#1631, parked form: the stream's HEADERS were encoded
18548 /// by a write pass, but the socket took none of it, so the block is
18549 /// parked in `front.out` (the #1627 situation on `Position::Client`).
18550 /// The backend has still not seen the stream: no RST for it, and since
18551 /// #1630 the dropped block resets the encoder's table, so the next
18552 /// stream's block still decodes on the backend.
18553 ///
18554 /// TO SEE THIS RED: same edit as
18555 /// `a_backend_stream_ended_before_its_headers_left_is_not_reset`. The
18556 /// backend then reads a RST_STREAM for stream 1, which it never opened:
18557 /// `no frame may name stream 1`. Red on `d5161919`, verified 2026-09-28.
18558 #[test]
18559 fn a_backend_stream_ended_with_its_headers_parked_is_not_reset() {
18560 let pool = make_pool_for_invariant_16();
18561 let (socket, _peer) = connected_socket();
18562 let backend = Rc::new(RefCell::new(crate::backends::Backend::new(
18563 "parked-backend",
18564 "127.0.0.1:2".parse().expect("backend address must parse"),
18565 None,
18566 None,
18567 None,
18568 )));
18569 let mut registry = crate::protocol::mux::BackendRegistry::default();
18570 let mut connection = H2Shell::new(
18571 Ulid::generate(),
18572 PacedSocket {
18573 stream: socket,
18574 budget: 0,
18575 wire: Vec::new(),
18576 inbound: std::collections::VecDeque::new(),
18577 },
18578 Position::Client(
18579 "parked-cluster".into(),
18580 registry.id_for(&backend),
18581 BackendStatus::Connected,
18582 ),
18583 &mut PoolBufferSource::new(Rc::downgrade(&pool)),
18584 H2FloodConfig::default(),
18585 H2ConnectionConfig::default(),
18586 Duration::from_secs(30),
18587 None,
18588 Duration::from_secs(30),
18589 Some((H2StreamId::Zero, 9)),
18590 Ready::READABLE | Ready::WRITABLE | Ready::HUP | Ready::ERROR,
18591 )
18592 .expect("a pool with free buffers must yield an H2 connection");
18593 connection.core.state = H2State::Header;
18594 let mut context = test_context(&pool);
18595 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
18596 let first = context
18597 .create_stream(Ulid::generate(), 1 << 16)
18598 .expect("test context must create a stream");
18599 queue_request(&mut context, first);
18600 assert!(
18601 connection.start_stream(first, &mut context),
18602 "premise: stream 1"
18603 );
18604 connection.core.readiness.event.insert(Ready::WRITABLE);
18605 connection.writable(&mut context, EndpointClient(&mut router));
18606 assert!(
18607 connection.socket.wire.is_empty()
18608 && !context.streams[first].front.out.is_empty()
18609 && !context.streams[first].front.consumed,
18610 "premise: stream 1's HEADERS were encoded and parked unsent"
18611 );
18612
18613 connection.end_stream(first, &mut context);
18614 let third = context
18615 .create_stream(Ulid::generate(), 1 << 16)
18616 .expect("test context must create a stream");
18617 queue_request(&mut context, third);
18618 assert!(
18619 connection.start_stream(third, &mut context),
18620 "premise: stream 3"
18621 );
18622 connection.socket.budget = usize::MAX;
18623 drive_both_ways(&mut connection, &mut context, &mut router);
18624
18625 let frames = peer_frames(&connection.socket.wire).expect("whole frames");
18626 assert!(
18627 frames.iter().all(|(_, _, id, _)| *id != 1),
18628 "no frame may name stream 1, which the backend never saw: {frames:?}"
18629 );
18630 let mut decoder = crate::protocol::mux::hpack::Decoder::new();
18631 let decoded: Vec<(u32, bool)> = frames
18632 .iter()
18633 .filter(|(kind, ..)| *kind == 1)
18634 .map(|(_, _, id, payload)| (*id, decoder.decode_with_cb(payload, |_, _| {}).is_ok()))
18635 .collect();
18636 assert_eq!(
18637 decoded,
18638 vec![(3, true)],
18639 "stream 3's block must decode with the backend's own table"
18640 );
18641 }
18642
18643 /// #1597, with #1604's split: skipping an orphaned remainder turns
18644 /// READABLE back on even while output is queued, and leaves that output
18645 /// alone. The skip reads into `zero`, which is input only, so there is
18646 /// nothing to wait for; the queued frame still leaves in order.
18647 ///
18648 /// Replaces the pre-#1604 test that pinned the opposite (READABLE kept
18649 /// off while zero held output), for the pre-image, where `zero` also held
18650 /// our output and a read into it would have parsed that output.
18651 ///
18652 /// TO SEE THIS RED: in [`ConnectionH2::skip_orphaned_data_payload`], only
18653 /// insert READABLE when `self.output.is_empty()`. The test fails with
18654 /// `the skip must read on while output is queued`.
18655 #[test]
18656 fn skipping_an_orphaned_payload_reads_on_while_output_is_queued() {
18657 let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 20, 16_384)));
18658 let (mut connection, _peer) = test_h2_connection(&pool, None);
18659 let window_update = orphan_frame(8, 0, 1, 4, &16_384u32.to_be_bytes());
18660 connection.core.output.push(&window_update);
18661 connection.core.readiness.interest.remove(Ready::READABLE);
18662
18663 connection.core.skip_orphaned_data_payload(6, 10);
18664
18665 assert!(
18666 connection.core.readiness.interest.is_readable(),
18667 "the skip must read on while output is queued"
18668 );
18669 assert_eq!(
18670 connection.core.stream_table.expect_read(),
18671 Some((H2StreamId::Zero, 6)),
18672 "the remainder must still be scheduled for skipping"
18673 );
18674 assert_eq!(
18675 connection.core.output_pending(),
18676 &window_update[..],
18677 "the skip must leave the queued output untouched"
18678 );
18679 }
18680
18681 /// A backend (client-side) H2 connection starts write-only, sends its
18682 /// preface and SETTINGS, and must then READ, for the server's SETTINGS.
18683 /// The pre-image turned READABLE on as a side effect of flushing `zero`;
18684 /// with the output queue separate from the input, the
18685 /// `(ClientSettings, Client)` arm turns it on itself.
18686 ///
18687 /// TO SEE THIS RED: in [`ConnectionH2::dispatch_writable_state`]'s
18688 /// `(H2State::ClientSettings, Position::Client(..))` arm, delete
18689 /// `self.readiness.interest.insert(Ready::READABLE);`. The test fails
18690 /// with `a backend connection must read once its preface is out`.
18691 #[test]
18692 fn a_backend_connection_reads_once_its_preface_is_out() {
18693 use std::io::Read;
18694
18695 let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 20, 16_384)));
18696 let (socket, mut peer) = connected_socket();
18697 let backend = Rc::new(RefCell::new(crate::backends::Backend::new(
18698 "preface-backend",
18699 "127.0.0.1:2".parse().expect("backend address must parse"),
18700 None,
18701 None,
18702 None,
18703 )));
18704 let mut registry = crate::protocol::mux::BackendRegistry::default();
18705 let mut connection = H2Shell::new(
18706 Ulid::generate(),
18707 socket,
18708 Position::Client(
18709 "preface-cluster".into(),
18710 registry.id_for(&backend),
18711 BackendStatus::Connected,
18712 ),
18713 &mut PoolBufferSource::new(Rc::downgrade(&pool)),
18714 H2FloodConfig::default(),
18715 H2ConnectionConfig::default(),
18716 Duration::from_secs(30),
18717 None,
18718 Duration::from_secs(30),
18719 None,
18720 Ready::WRITABLE | Ready::HUP | Ready::ERROR,
18721 )
18722 .expect("a pool with free buffers must yield an H2 connection");
18723 let mut context = test_context(&pool);
18724 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
18725
18726 for _ in 0..2 {
18727 connection.core.readiness.event.insert(Ready::WRITABLE);
18728 connection.writable(&mut context, EndpointClient(&mut router));
18729 }
18730
18731 let mut preface = [0u8; 24];
18732 peer.read_exact(&mut preface)
18733 .expect("the backend must receive the client preface");
18734 assert_eq!(&preface[..], serializer::H2_PRI.as_bytes());
18735 assert!(
18736 matches!(connection.core.state, H2State::ServerSettings),
18737 "premise: the preface went out, got {:?}",
18738 connection.core.state
18739 );
18740 assert!(
18741 connection.core.readiness.interest.is_readable(),
18742 "a backend connection must read once its preface is out"
18743 );
18744 }
18745
18746 /// A peer that sends while never reading cannot grow the output queue
18747 /// without bound: once one buffer's worth is queued, reading stops, as
18748 /// HAProxy's demux stops on a full `mbuf` (`H2_CF_DEM_MROOM`), and it
18749 /// resumes once the queue drains below that.
18750 ///
18751 /// TO SEE THIS RED: in [`ConnectionH2::poll_read_target`], delete the
18752 /// `self.output.len() >= self.output_read_cap()` early return. The test
18753 /// fails with `a full output queue must stop reading`.
18754 #[test]
18755 fn a_full_output_queue_stops_reading_until_it_drains() {
18756 let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 20, 16_384)));
18757 let (mut connection, _peer) = test_h2_connection(&pool, None);
18758 let mut context = test_context(&pool);
18759 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
18760 connection.core.state = H2State::Header;
18761 connection
18762 .core
18763 .stream_table
18764 .set_expect_read(Some((H2StreamId::Zero, 9)));
18765 let cap = connection.core.output_read_cap();
18766 connection.core.output.push(&vec![0; cap]);
18767 connection.core.readiness.interest.insert(Ready::READABLE);
18768 connection.core.readiness.event.insert(Ready::READABLE);
18769
18770 connection.readable(&mut context, EndpointClient(&mut router));
18771
18772 assert!(
18773 !connection.core.readiness.interest.is_readable(),
18774 "a full output queue must stop reading"
18775 );
18776 assert!(
18777 connection.core.readiness.event.is_readable(),
18778 "the READABLE event must be kept for the read that follows the drain"
18779 );
18780 connection.core.consume_output(1);
18781 assert!(
18782 connection.core.readiness.interest.is_readable(),
18783 "draining below the cap must restore reading"
18784 );
18785 }
18786
18787 /// Clearing the output queue, as a peer that hung up while the
18788 /// connection drains does, also ends the read cap's hold on READABLE:
18789 /// otherwise `reads_wait_for_output` would outlive the output it waited on
18790 /// and the shutdown drive's reads would stay withdrawn.
18791 ///
18792 /// TO SEE THIS RED: in `ConnectionH2::clear_output`, drop everything but
18793 /// `self.output.clear();`. The test then fails with `a cleared queue must
18794 /// release the read cap`. Verified 2026-09-27.
18795 #[test]
18796 fn clearing_the_output_releases_the_read_cap() {
18797 let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 20, 16_384)));
18798 let (mut connection, _peer) = test_h2_connection(&pool, None);
18799 let mut context = test_context(&pool);
18800 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
18801 connection.core.state = H2State::Header;
18802 connection
18803 .core
18804 .stream_table
18805 .set_expect_read(Some((H2StreamId::Zero, 9)));
18806 let cap = connection.core.output_read_cap();
18807 connection.core.output.push(&vec![0; cap]);
18808 connection.core.readiness.interest.insert(Ready::READABLE);
18809 connection.core.readiness.event.insert(Ready::READABLE);
18810 connection.readable(&mut context, EndpointClient(&mut router));
18811 assert!(
18812 connection.core.reads_wait_for_output
18813 && !connection.core.readiness.interest.is_readable(),
18814 "premise: the read cap withdrew READABLE"
18815 );
18816
18817 connection.core.clear_output();
18818
18819 assert!(
18820 !connection.core.reads_wait_for_output
18821 && connection.core.readiness.interest.is_readable(),
18822 "a cleared queue must release the read cap"
18823 );
18824 }
18825
18826 /// #1597, with #1604's split: a frame header read while output is still
18827 /// queued lands in `zero` and leaves that output intact, and the answer
18828 /// to the frame read queues BEHIND it. A stalled WINDOW_UPDATE flush
18829 /// leaves exactly that state; the CI e2e suite once caught the
18830 /// pre-image, where both shared `zero`, through `handle_header_state`'s
18831 /// `debug_assert!`, with `zero` holding a stream-1 WINDOW_UPDATE followed
18832 /// by the peer's next DATA header.
18833 ///
18834 /// Replaces the pre-#1604 test that pinned the pre-image's cure (no frame
18835 /// header read at all while zero held unflushed output until the flush):
18836 /// the hazard it guarded no longer exists, and what the peer must receive
18837 /// is unchanged and still asserted below.
18838 ///
18839 /// TO SEE THIS RED: in [`ConnectionH2::handle_ping_frame`], queue the ACK
18840 /// in front of the output instead of behind it (`self.output.clear()`
18841 /// before `push_frames`). The test fails with `the PING ACK must queue
18842 /// behind the WINDOW_UPDATE`.
18843 #[test]
18844 fn a_frame_header_read_leaves_queued_output_intact() {
18845 use std::io::{Read, Write};
18846
18847 let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 20, 16_384)));
18848 let (mut connection, mut peer) = test_h2_connection(&pool, None);
18849 peer.set_nonblocking(true)
18850 .expect("the loopback peer must switch to non-blocking");
18851 let mut context = test_context(&pool);
18852 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
18853 connection.core.state = H2State::Header;
18854 connection
18855 .core
18856 .stream_table
18857 .set_expect_read(Some((H2StreamId::Zero, 9)));
18858
18859 // What a stalled WINDOW_UPDATE flush leaves behind.
18860 let window_update = orphan_frame(8, 0, 1, 4, &16_384u32.to_be_bytes());
18861 connection.core.output.push(&window_update);
18862 connection.core.readiness.interest.insert(Ready::READABLE);
18863
18864 peer.write_all(&orphan_ping())
18865 .expect("loopback write must complete");
18866 let mut expected = window_update.clone();
18867 expected.extend(orphan_frame(6, parser::FLAG_ACK, 0, 8, b"pingpong"));
18868 for _ in 0..64 {
18869 connection.core.readiness.event.insert(Ready::READABLE);
18870 connection.readable(&mut context, EndpointClient(&mut router));
18871 if connection.core.output.len() >= expected.len() {
18872 break;
18873 }
18874 std::thread::yield_now();
18875 }
18876 assert_eq!(
18877 connection.core.output_pending(),
18878 &expected[..],
18879 "the PING ACK must queue behind the WINDOW_UPDATE"
18880 );
18881
18882 // Driven the way `Mux` does, the queue leaves in order.
18883 let mut received = Vec::new();
18884 for _ in 0..256 {
18885 connection
18886 .core
18887 .readiness
18888 .event
18889 .insert(Ready::READABLE | Ready::WRITABLE);
18890 connection.core.readiness.interest.insert(Ready::WRITABLE);
18891 if connection.core.readiness.filter_interest().is_writable() {
18892 connection.writable(&mut context, EndpointClient(&mut router));
18893 }
18894 if connection.core.readiness.filter_interest().is_readable() {
18895 connection.readable(&mut context, EndpointClient(&mut router));
18896 }
18897 let mut buf = [0u8; 256];
18898 if let Ok(n) = peer.read(&mut buf) {
18899 received.extend_from_slice(&buf[..n]);
18900 }
18901 if received.len() >= expected.len() {
18902 break;
18903 }
18904 std::thread::yield_now();
18905 }
18906 assert_eq!(
18907 received, expected,
18908 "the peer must receive the WINDOW_UPDATE, then the PING ACK"
18909 );
18910 }
18911
18912 /// The RED-confirmed test above sends a HEADERS payload that is nothing
18913 /// but a field block: no `Pad Length`, no `Stream Dependency`/`Weight`.
18914 /// That shape cannot catch correction #1 — RFC 9113 §6.2 lays a HEADERS
18915 /// payload out as `[Pad Length?][Priority?][field block][padding?]`, and
18916 /// `refuse_stream_and_discard`'s new-stream callers pass the *whole*
18917 /// payload. A decode that forgets to strip the PADDED/PRIORITY prefix
18918 /// feeds the HPACK decoder six bytes of pad-length/dependency/weight
18919 /// garbage before the real field block starts.
18920 ///
18921 /// To SEE THIS RED: in [`decode_discarded_field_block`]'s
18922 /// `DiscardedFieldBlock::New` arm, replace the `let fragment = headers
18923 /// .header_block_fragment.data_opt(payload)...` lines with `let fragment
18924 /// = payload;` — i.e. hand the connection decoder the raw payload
18925 /// unstripped, the way a fix that only handled the un-padded case would.
18926 /// The peer's first block is still legitimate HPACK once you skip past
18927 /// the 6-byte prefix, but decoding from byte 0 instead makes the pad
18928 /// length byte and the first three dependency/weight octets look like
18929 /// HPACK opcodes. Verified 2026-09-21: the decode fails with
18930 /// `H2Error::CompressionError`, observed via the connection reaching
18931 /// `H2State::GoAway` (the panic is on the "connection usable" assertion,
18932 /// not the later decode assertion — the malformed prefix corrupts the
18933 /// connection before the probe even gets to send its second block).
18934 #[test]
18935 fn a_refused_padded_prioritized_stream_keeps_the_hpack_decoder_in_sync() {
18936 use std::io::Write;
18937
18938 let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 20, 16_384)));
18939 let (mut connection, mut peer) = test_h2_connection(&pool, None);
18940 let mut context = test_context(&pool);
18941 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
18942
18943 connection.core.state = H2State::Header;
18944 connection
18945 .core
18946 .stream_table
18947 .set_expect_read(Some((H2StreamId::Zero, 9)));
18948 connection.core.drain.__test_set_draining();
18949
18950 // Same probe technique as the sibling test: this appends
18951 // `x-sozu-probe: alpha` to the peer's dynamic table at index 62.
18952 let mut peer_encoder = crate::protocol::mux::hpack::Encoder::new();
18953 let field_block = peer_encoder.encode([
18954 (&b":method"[..], &b"GET"[..]),
18955 (&b":scheme"[..], &b"https"[..]),
18956 (&b":authority"[..], &b"example.com"[..]),
18957 (&b":path"[..], &b"/refused"[..]),
18958 (&b"x-sozu-probe"[..], &b"alpha"[..]),
18959 ]);
18960
18961 // RFC 9113 §6.2 HEADERS payload: Pad Length (1) + Stream Dependency
18962 // (4, exclusive bit clear, depends on stream 0) + Weight (1) + the
18963 // field block + 3 zero padding bytes.
18964 const PAD_LEN: u8 = 3;
18965 let mut payload = Vec::with_capacity(1 + 5 + field_block.len() + PAD_LEN as usize);
18966 payload.push(PAD_LEN);
18967 payload.extend_from_slice(&0u32.to_be_bytes()); // Stream Dependency
18968 payload.push(15); // Weight
18969 payload.extend_from_slice(&field_block);
18970 payload.extend(std::iter::repeat_n(0u8, PAD_LEN as usize));
18971
18972 // HEADERS, END_STREAM | END_HEADERS | PADDED | PRIORITY, stream 1.
18973 let mut frame = Vec::with_capacity(9 + payload.len());
18974 frame.extend_from_slice(&(payload.len() as u32).to_be_bytes()[1..]);
18975 frame.push(1);
18976 frame.push(
18977 parser::FLAG_END_STREAM
18978 | parser::FLAG_END_HEADERS
18979 | parser::FLAG_PADDED
18980 | parser::FLAG_PRIORITY,
18981 );
18982 frame.extend_from_slice(&1u32.to_be_bytes());
18983 frame.extend_from_slice(&payload);
18984 peer.write_all(&frame)
18985 .expect("loopback write must complete");
18986 peer.flush().expect("loopback flush must complete");
18987
18988 for _ in 0..64 {
18989 connection.readable(&mut context, EndpointClient(&mut router));
18990 if matches!(connection.core.state, H2State::Header)
18991 && matches!(
18992 connection.core.stream_table.expect_read(),
18993 Some((H2StreamId::Zero, 9))
18994 )
18995 && !connection.core.control_tx.pending().is_empty()
18996 {
18997 break;
18998 }
18999 std::thread::yield_now();
19000 }
19001
19002 assert_eq!(
19003 connection.core.control_tx.pending(),
19004 vec![(1, H2Error::RefusedStream)],
19005 "the drain gate must refuse stream 1 with RST_STREAM(REFUSED_STREAM)"
19006 );
19007 assert!(
19008 connection.core.stream_table.is_empty(),
19009 "a refused stream must not be created"
19010 );
19011 assert!(
19012 !matches!(connection.core.state, H2State::Error | H2State::GoAway),
19013 "refusing a PADDED|PRIORITY HEADERS frame must leave the connection \
19014 usable: {:?}",
19015 connection.core.state
19016 );
19017
19018 let next_block = peer_encoder.encode([(&b"x-sozu-probe"[..], &b"alpha"[..])]);
19019 assert_eq!(
19020 next_block.len(),
19021 1,
19022 "the peer must now reference its dynamic entry, not re-send a literal"
19023 );
19024
19025 let mut decoded = Vec::new();
19026 let status = connection
19027 .core
19028 .hpack
19029 .decoder_mut()
19030 .decode_with_cb(&next_block, |k, v| {
19031 decoded.push((k.into_owned(), v.into_owned()));
19032 });
19033
19034 assert!(
19035 status.is_ok(),
19036 "the connection decoder must still resolve the peer's dynamic table \
19037 after a refused PADDED|PRIORITY stream, got {status:?}"
19038 );
19039 assert_eq!(
19040 decoded,
19041 vec![(b"x-sozu-probe".to_vec(), b"alpha".to_vec())],
19042 "the refused stream's field block — located past the Pad Length and \
19043 Priority prefix — must have been decoded into the connection's \
19044 HPACK context"
19045 );
19046 }
19047
19048 // ── `self.zero` is read-accumulation AND write scratch — that is the bug ──
19049 //
19050 // `ConnectionH2::zero` serves two roles: the read-side accumulation
19051 // buffer for a HEADERS/CONTINUATION field block in progress, and the
19052 // write scratch space `flush_pending_control_frames` reuses to
19053 // serialise ANY queued WINDOW_UPDATE or RST_STREAM — for any stream,
19054 // not just the one being reassembled. `mod.rs`'s inner event loop
19055 // dispatches frontend `readable()` then `writable()` in the same sweep,
19056 // so a legitimate, non-refused multi-frame header block can be
19057 // clobbered by completely unrelated connection-level flow-control
19058 // housekeeping before its CONTINUATION frame ever arrives — no
19059 // adversarial peer required.
19060 //
19061 // That was the pre-image of #1396/#1397/#1401 and, for the write
19062 // scratch, of #1604: the accumulator moved to `header_reassembly`, and
19063 // every control frame is now queued in the separate `output`. The tests
19064 // below keep pinning the outcome, a block that survives every
19065 // interleaved flush; their RED notes describe the tree they were written
19066 // against.
19067
19068 /// To SEE THIS RED: this is the pre-existing behaviour on `main`, no
19069 /// mutation needed. `flush_pending_control_frames`'s WINDOW_UPDATE-drain
19070 /// stage (`let kawa = &mut self.zero; kawa.storage.clear();`) runs
19071 /// unconditionally whenever a WINDOW_UPDATE is queued, with no check on
19072 /// `self.state`. Guarding that stage (and the RST_STREAM-drain stage
19073 /// beneath it) on `matches!(self.state, H2State::ContinuationHeader(_) |
19074 /// H2State::ContinuationFrame(_))` is exactly the fix this test proves.
19075 #[test]
19076 fn a_legitimate_continuation_survives_an_unrelated_window_update_flush() {
19077 use std::io::Write;
19078
19079 let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 20, 16_384)));
19080 let (mut connection, mut peer) = test_h2_connection(&pool, None);
19081 let mut context = test_context(&pool);
19082 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
19083
19084 // Past the preface/SETTINGS handshake, waiting on a frame header.
19085 // `drain.draining` stays false: stream 1 must be genuinely accepted,
19086 // not refused — this is the ordinary, non-refusal path.
19087 connection.core.state = H2State::Header;
19088 connection
19089 .core
19090 .stream_table
19091 .set_expect_read(Some((H2StreamId::Zero, 9)));
19092
19093 // The peer's encoder. The marker header's name is in neither table,
19094 // so encoding it appends `x-sozu-marker: legitimate-value` to the
19095 // peer's dynamic table at a known index — the same technique
19096 // `a_refused_stream_keeps_the_hpack_decoder_in_sync` uses to prove
19097 // decoder sync, here applied to a stream that is never refused.
19098 let mut peer_encoder = crate::protocol::mux::hpack::Encoder::new();
19099 let field_block = peer_encoder.encode([
19100 (&b":method"[..], &b"GET"[..]),
19101 (&b":scheme"[..], &b"https"[..]),
19102 (&b":authority"[..], &b"example.com"[..]),
19103 (&b":path"[..], &b"/legit-multiframe"[..]),
19104 (&b"x-sozu-marker"[..], &b"legitimate-value"[..]),
19105 ]);
19106 // Split well past the 13 bytes a single WINDOW_UPDATE frame occupies,
19107 // so the clobber this test provokes lands inside real field-block
19108 // bytes rather than in a margin this test invented.
19109 assert!(
19110 field_block.len() > 26,
19111 "the probe block must be large enough to split meaningfully"
19112 );
19113 let split = field_block.len() / 2;
19114 let (first_half, second_half) = field_block.split_at(split);
19115
19116 // HEADERS, END_STREAM but NOT END_HEADERS, stream 1: a legitimate
19117 // multi-frame header block, exactly as a large request produces.
19118 let mut headers_frame = Vec::with_capacity(9 + first_half.len());
19119 headers_frame.extend_from_slice(&(first_half.len() as u32).to_be_bytes()[1..]);
19120 headers_frame.push(1); // HEADERS
19121 headers_frame.push(parser::FLAG_END_STREAM);
19122 headers_frame.extend_from_slice(&1u32.to_be_bytes());
19123 headers_frame.extend_from_slice(first_half);
19124 peer.write_all(&headers_frame)
19125 .expect("loopback write must complete");
19126 peer.flush().expect("loopback flush must complete");
19127
19128 // Drive reads until the connection is genuinely waiting on the
19129 // CONTINUATION frame for stream 1 — accepted, not refused.
19130 for _ in 0..64 {
19131 connection.readable(&mut context, EndpointClient(&mut router));
19132 if matches!(connection.core.state, H2State::ContinuationHeader(_)) {
19133 break;
19134 }
19135 std::thread::yield_now();
19136 }
19137 assert!(
19138 matches!(connection.core.state, H2State::ContinuationHeader(_)),
19139 "stream 1's HEADERS frame must be accepted and await CONTINUATION: {:?}",
19140 connection.core.state
19141 );
19142 assert!(
19143 !connection.core.stream_table.is_empty(),
19144 "a non-refused HEADERS frame must create stream 1"
19145 );
19146
19147 // Completely unrelated connection-level housekeeping: replenishing
19148 // the connection flow-control window after DATA consumed on some
19149 // other stream. Nothing about it targets stream 1 or its
19150 // in-progress header block — this is the ordinary path, not an
19151 // attack.
19152 connection.core.queue_window_update(0, 65_535);
19153 connection.writable(&mut context, EndpointClient(&mut router));
19154
19155 // Now the CONTINUATION frame completing the block arrives.
19156 let mut continuation_frame = Vec::with_capacity(9 + second_half.len());
19157 continuation_frame.extend_from_slice(&(second_half.len() as u32).to_be_bytes()[1..]);
19158 continuation_frame.push(9); // CONTINUATION
19159 continuation_frame.push(parser::FLAG_END_HEADERS);
19160 continuation_frame.extend_from_slice(&1u32.to_be_bytes());
19161 continuation_frame.extend_from_slice(second_half);
19162 peer.write_all(&continuation_frame)
19163 .expect("loopback write must complete");
19164 peer.flush().expect("loopback flush must complete");
19165
19166 for _ in 0..64 {
19167 connection.readable(&mut context, EndpointClient(&mut router));
19168 if !matches!(
19169 connection.core.state,
19170 H2State::ContinuationHeader(_) | H2State::ContinuationFrame(_)
19171 ) {
19172 break;
19173 }
19174 std::thread::yield_now();
19175 }
19176
19177 assert!(
19178 !matches!(connection.core.state, H2State::Error | H2State::GoAway),
19179 "an unrelated WINDOW_UPDATE flush must not corrupt a legitimate \
19180 in-progress header block into a connection error: {:?}",
19181 connection.core.state
19182 );
19183
19184 // The peer's next block references the dynamic entry its encoder
19185 // added while encoding stream 1's request.
19186 let next_block = peer_encoder.encode([(&b"x-sozu-marker"[..], &b"legitimate-value"[..])]);
19187 assert_eq!(
19188 next_block.len(),
19189 1,
19190 "the peer must now reference its dynamic entry, not re-send a literal"
19191 );
19192
19193 let mut decoded = Vec::new();
19194 let status = connection
19195 .core
19196 .hpack
19197 .decoder_mut()
19198 .decode_with_cb(&next_block, |k, v| {
19199 decoded.push((k.into_owned(), v.into_owned()));
19200 });
19201
19202 assert!(
19203 status.is_ok(),
19204 "the connection decoder must still resolve the peer's dynamic table \
19205 after an unrelated WINDOW_UPDATE flush during header-block \
19206 reassembly, got {status:?}"
19207 );
19208 assert_eq!(
19209 decoded,
19210 vec![(b"x-sozu-marker".to_vec(), b"legitimate-value".to_vec())],
19211 "stream 1's field block must have been decoded byte-for-byte, not \
19212 clobbered by the unrelated WINDOW_UPDATE flush"
19213 );
19214 }
19215
19216 /// Same shape as
19217 /// `a_legitimate_continuation_survives_an_unrelated_window_update_flush`,
19218 /// but the clobbering trigger is `graceful_goaway` itself instead of an
19219 /// unrelated WINDOW_UPDATE flush — the third instance of the same bug
19220 /// class (issue #1398).
19221 ///
19222 /// Since #1637 the advisory GOAWAY is no longer deferred while the block
19223 /// reassembles: it is queued in `self.output` and flushed in the same
19224 /// pass, and this test checks both halves on the wire — the peer reads
19225 /// the 17-byte `GOAWAY(NO_ERROR, STREAM_ID_MAX)` BEFORE it sends the
19226 /// CONTINUATION, and the block that CONTINUATION completes still decodes
19227 /// byte-for-byte afterwards.
19228 ///
19229 /// To SEE THIS RED, either reintroduce the #1401 deferral (hold the
19230 /// GOAWAY back while `H2State::ContinuationHeader` holds) — the
19231 /// `read_exact` of the advisory GOAWAY before the CONTINUATION finds nothing
19232 /// — or serialize the GOAWAY into `self.zero.storage` as the pre-#1604
19233 /// code did, which corrupts the block and fails the decode assertions.
19234 #[test]
19235 fn a_legitimate_continuation_survives_a_graceful_goaway() {
19236 use std::io::{Read, Write};
19237
19238 let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 20, 16_384)));
19239 let (mut connection, mut peer) = test_h2_connection(&pool, None);
19240 let mut context = test_context(&pool);
19241 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
19242
19243 // Past the preface/SETTINGS handshake, waiting on a frame header.
19244 // `drain.draining` stays false until `graceful_goaway` is called
19245 // below: stream 1 must be genuinely accepted, not refused — this is
19246 // the ordinary, non-refusal path.
19247 connection.core.state = H2State::Header;
19248 connection
19249 .core
19250 .stream_table
19251 .set_expect_read(Some((H2StreamId::Zero, 9)));
19252
19253 // The peer's encoder. The marker header's name is in neither table,
19254 // so encoding it appends `x-sozu-marker: legitimate-value` to the
19255 // peer's dynamic table at a known index — the same technique
19256 // `a_legitimate_continuation_survives_an_unrelated_window_update_flush`
19257 // uses to prove decoder sync.
19258 let mut peer_encoder = crate::protocol::mux::hpack::Encoder::new();
19259 let field_block = peer_encoder.encode([
19260 (&b":method"[..], &b"GET"[..]),
19261 (&b":scheme"[..], &b"https"[..]),
19262 (&b":authority"[..], &b"example.com"[..]),
19263 (&b":path"[..], &b"/legit-multiframe"[..]),
19264 (&b"x-sozu-marker"[..], &b"legitimate-value"[..]),
19265 ]);
19266 // Same header set and split proportions as
19267 // `a_legitimate_continuation_survives_an_unrelated_window_update_flush`:
19268 // `header_block_fragment`'s window is a `(start, len)` pair recorded
19269 // once, independent of `self.zero.storage`'s own bookkeeping, so
19270 // *any* clear-then-refill of that buffer — 13 bytes of WINDOW_UPDATE
19271 // or 17 bytes of GOAWAY, it makes no difference — leaves the window
19272 // pointing at the same wrong offset once `end`/`head` reset to 0 and
19273 // the CONTINUATION's own bytes land there instead. Matching the split
19274 // reproduces the same decodable-but-wrong HPACK byte sequence rather
19275 // than a split that happens to land on an invalid opcode.
19276 assert!(
19277 field_block.len() > 34,
19278 "the probe block must be large enough to split meaningfully"
19279 );
19280 let split = field_block.len() / 2;
19281 let (first_half, second_half) = field_block.split_at(split);
19282
19283 // HEADERS, END_STREAM but NOT END_HEADERS, stream 1: a legitimate
19284 // multi-frame header block, exactly as a large request produces.
19285 let mut headers_frame = Vec::with_capacity(9 + first_half.len());
19286 headers_frame.extend_from_slice(&(first_half.len() as u32).to_be_bytes()[1..]);
19287 headers_frame.push(1); // HEADERS
19288 headers_frame.push(parser::FLAG_END_STREAM);
19289 headers_frame.extend_from_slice(&1u32.to_be_bytes());
19290 headers_frame.extend_from_slice(first_half);
19291 peer.write_all(&headers_frame)
19292 .expect("loopback write must complete");
19293 peer.flush().expect("loopback flush must complete");
19294
19295 // Drive reads until the connection is genuinely waiting on the
19296 // CONTINUATION frame for stream 1 — accepted, not refused.
19297 for _ in 0..64 {
19298 connection.readable(&mut context, EndpointClient(&mut router));
19299 if matches!(connection.core.state, H2State::ContinuationHeader(_)) {
19300 break;
19301 }
19302 std::thread::yield_now();
19303 }
19304 assert!(
19305 matches!(connection.core.state, H2State::ContinuationHeader(_)),
19306 "stream 1's HEADERS frame must be accepted and await CONTINUATION: {:?}",
19307 connection.core.state
19308 );
19309 assert!(
19310 !connection.core.stream_table.is_empty(),
19311 "a non-refused HEADERS frame must create stream 1"
19312 );
19313
19314 // The proxy decides to drain — a graceful shutdown or hot reload
19315 // landing mid-reassembly, exactly as issue #1398 describes. Nothing
19316 // about it targets stream 1 or its in-progress header block.
19317 let drain_at = connection.core.now;
19318 let result = connection.graceful_goaway(drain_at);
19319 assert!(
19320 matches!(result, MuxResult::Continue),
19321 "graceful_goaway must not tear the connection down mid-reassembly: {result:?}"
19322 );
19323
19324 // The forced-close budget must arm from the moment the proxy decided
19325 // to drain — `Mux::shutting_down` samples exactly this field against
19326 // `graceful_shutdown_deadline`.
19327 assert!(
19328 connection.core.drain.draining(),
19329 "graceful_goaway must mark the connection as draining immediately"
19330 );
19331 assert_eq!(
19332 connection.core.drain.__test_started_at(),
19333 Some(drain_at),
19334 "the forced-close budget must arm from `graceful_goaway`'s `now` parameter"
19335 );
19336 assert!(
19337 matches!(connection.core.state, H2State::ContinuationHeader(_)),
19338 "queueing the advisory GOAWAY must leave the reassembly state alone: {:?}",
19339 connection.core.state
19340 );
19341
19342 // Mirror `Mux::shutting_down_inner` exactly: it calls
19343 // `flush_output_buffer()` immediately after `graceful_goaway()`
19344 // returns `Continue`, because edge-triggered epoll won't deliver a
19345 // fresh WRITABLE event for an already-writable socket. The advisory
19346 // GOAWAY leaves in this flush, while the block is still incomplete.
19347 connection.flush_output_buffer();
19348 assert!(
19349 connection.core.output.is_empty(),
19350 "the advisory GOAWAY must have been fully flushed to the socket"
19351 );
19352
19353 let mut expected = [0u8; 17];
19354 let (_, expected_size) =
19355 serializer::gen_goaway(&mut expected, STREAM_ID_MAX, H2Error::NoError)
19356 .expect("serializing the expected GOAWAY must succeed");
19357 assert_eq!(expected_size, 17);
19358
19359 let mut received = [0u8; 17];
19360 peer.set_read_timeout(Some(Duration::from_secs(5)))
19361 .expect("setting a read timeout must succeed");
19362 peer.read_exact(&mut received)
19363 .expect("the peer must receive the advisory GOAWAY before it completes the block");
19364 assert_eq!(
19365 received, expected,
19366 "the advisory GOAWAY must reach the peer byte-for-byte while its \
19367 header block is still incomplete"
19368 );
19369
19370 // Now the CONTINUATION frame completing the block arrives.
19371 let mut continuation_frame = Vec::with_capacity(9 + second_half.len());
19372 continuation_frame.extend_from_slice(&(second_half.len() as u32).to_be_bytes()[1..]);
19373 continuation_frame.push(9); // CONTINUATION
19374 continuation_frame.push(parser::FLAG_END_HEADERS);
19375 continuation_frame.extend_from_slice(&1u32.to_be_bytes());
19376 continuation_frame.extend_from_slice(second_half);
19377 peer.write_all(&continuation_frame)
19378 .expect("loopback write must complete");
19379 peer.flush().expect("loopback flush must complete");
19380
19381 for _ in 0..64 {
19382 connection.readable(&mut context, EndpointClient(&mut router));
19383 if !matches!(
19384 connection.core.state,
19385 H2State::ContinuationHeader(_) | H2State::ContinuationFrame(_)
19386 ) {
19387 break;
19388 }
19389 std::thread::yield_now();
19390 }
19391
19392 assert!(
19393 !matches!(connection.core.state, H2State::Error | H2State::GoAway),
19394 "a graceful_goaway landing mid-reassembly must not corrupt a \
19395 legitimate in-progress header block into a connection error: {:?}",
19396 connection.core.state
19397 );
19398
19399 // The peer's next block references the dynamic entry its encoder
19400 // added while encoding stream 1's request.
19401 let next_block = peer_encoder.encode([(&b"x-sozu-marker"[..], &b"legitimate-value"[..])]);
19402 assert_eq!(
19403 next_block.len(),
19404 1,
19405 "the peer must now reference its dynamic entry, not re-send a literal"
19406 );
19407
19408 let mut decoded = Vec::new();
19409 let status = connection
19410 .core
19411 .hpack
19412 .decoder_mut()
19413 .decode_with_cb(&next_block, |k, v| {
19414 decoded.push((k.into_owned(), v.into_owned()));
19415 });
19416
19417 assert!(
19418 status.is_ok(),
19419 "the connection decoder must still resolve the peer's dynamic table \
19420 after a graceful_goaway during header-block reassembly, got {status:?}"
19421 );
19422 assert_eq!(
19423 decoded,
19424 vec![(b"x-sozu-marker".to_vec(), b"legitimate-value".to_vec())],
19425 "stream 1's field block must have been decoded byte-for-byte, not \
19426 clobbered by graceful_goaway"
19427 );
19428
19429 // Nothing else was owed to the peer: the drain is not a second
19430 // GOAWAY, and the completed block leaves nothing queued.
19431 assert!(
19432 connection.core.output.is_empty(),
19433 "completing the block must not queue a second advisory GOAWAY"
19434 );
19435 }
19436
19437 /// Regression test for #1423: `flush_pending_control_frames`'s
19438 /// `frontend_hung_up_while_draining()` stage cleared `zero.storage`
19439 /// unconditionally, with no `header_block_reassembly_in_progress()`
19440 /// guard — unlike its three sibling stages, which #1397 and #1401
19441 /// already gated. LIFECYCLE.md invariant 24 tracked this as a "known
19442 /// live gap, reported and NOT fixed" until this changeset.
19443 ///
19444 /// Same shape as `a_legitimate_continuation_survives_a_graceful_goaway`,
19445 /// but the clobbering trigger is a HUP/ERROR readiness event landing
19446 /// while draining — exactly `frontend_hung_up_while_draining()`'s own
19447 /// condition — instead of `graceful_goaway` itself. This test drives
19448 /// the gap BETWEEN two frames (a HUP arriving while
19449 /// `expect_read = Some((Zero, 9))`, waiting for the next CONTINUATION's
19450 /// header, with nothing of this block's history left in `zero.storage`
19451 /// to lose). Moving the accumulator out of `zero.storage` into
19452 /// `self.header_reassembly` (`h2_header_reassembly.rs`) makes THIS
19453 /// scenario safe regardless of whether this stage is guarded: there is
19454 /// no accumulated history left in `zero.storage` for it to clobber.
19455 ///
19456 /// That is NOT the whole of #1423, though — see
19457 /// `a_continuation_frame_split_by_tcp_segmentation_survives_a_hup_while_draining`
19458 /// below for the narrower "HUP lands mid-frame, not between frames"
19459 /// case this test does not cover, which review of this changeset's
19460 /// first version (`e1c3c2fb`) proved the accumulator move alone does
19461 /// NOT make safe, and which is why `frontend_hung_up_while_draining()`'s
19462 /// stage carries the same `header_block_reassembly_in_progress()` guard
19463 /// as its three siblings after all.
19464 ///
19465 /// To SEE THIS RED on this step's base commit (`f76c8eb3`, before the
19466 /// accumulator moved out of `zero.storage`): run this test unmodified
19467 /// against that commit. `flush_pending_control_frames`'s first stage
19468 /// clears `zero.storage` — which, on that commit, still holds stream 1's
19469 /// entire accumulated first-half fragment — before the CONTINUATION
19470 /// frame completing the block arrives; the CONTINUATION's own bytes
19471 /// then land at the wrong (reset-to-zero) offset, and the connection's
19472 /// HPACK decoder desyncs from the peer's encoder. The failure is
19473 /// silent corruption, not a crash (HPACK carries no self-check): the
19474 /// `decode_with_cb` assertion below fails with a decode error, or
19475 /// succeeds but yields something other than
19476 /// `[(b"x-sozu-marker", b"legitimate-value")]`.
19477 #[test]
19478 fn a_legitimate_continuation_survives_a_hup_while_draining() {
19479 use std::io::Write;
19480
19481 let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 20, 16_384)));
19482 let (mut connection, mut peer) = test_h2_connection(&pool, None);
19483 let mut context = test_context(&pool);
19484 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
19485
19486 // Past the preface/SETTINGS handshake, waiting on a frame header.
19487 // `drain.draining` stays false until manually forced below: stream 1
19488 // must be genuinely accepted, not refused — this is the ordinary,
19489 // non-refusal path.
19490 connection.core.state = H2State::Header;
19491 connection
19492 .core
19493 .stream_table
19494 .set_expect_read(Some((H2StreamId::Zero, 9)));
19495
19496 // The peer's encoder. The marker header's name is in neither table,
19497 // so encoding it appends `x-sozu-marker: legitimate-value` to the
19498 // peer's dynamic table at a known index — the same technique the
19499 // sibling WINDOW_UPDATE/graceful_goaway tests use to prove decoder
19500 // sync.
19501 let mut peer_encoder = crate::protocol::mux::hpack::Encoder::new();
19502 let field_block = peer_encoder.encode([
19503 (&b":method"[..], &b"GET"[..]),
19504 (&b":scheme"[..], &b"https"[..]),
19505 (&b":authority"[..], &b"example.com"[..]),
19506 (&b":path"[..], &b"/legit-multiframe"[..]),
19507 (&b"x-sozu-marker"[..], &b"legitimate-value"[..]),
19508 ]);
19509 assert!(
19510 field_block.len() > 34,
19511 "the probe block must be large enough to split meaningfully"
19512 );
19513 let split = field_block.len() / 2;
19514 let (first_half, second_half) = field_block.split_at(split);
19515
19516 // HEADERS, END_STREAM but NOT END_HEADERS, stream 1: a legitimate
19517 // multi-frame header block, exactly as a large request produces.
19518 let mut headers_frame = Vec::with_capacity(9 + first_half.len());
19519 headers_frame.extend_from_slice(&(first_half.len() as u32).to_be_bytes()[1..]);
19520 headers_frame.push(1); // HEADERS
19521 headers_frame.push(parser::FLAG_END_STREAM);
19522 headers_frame.extend_from_slice(&1u32.to_be_bytes());
19523 headers_frame.extend_from_slice(first_half);
19524 peer.write_all(&headers_frame)
19525 .expect("loopback write must complete");
19526 peer.flush().expect("loopback flush must complete");
19527
19528 // Drive reads until the connection is genuinely waiting on the
19529 // CONTINUATION frame for stream 1 — accepted, not refused.
19530 for _ in 0..64 {
19531 connection.readable(&mut context, EndpointClient(&mut router));
19532 if matches!(connection.core.state, H2State::ContinuationHeader(_)) {
19533 break;
19534 }
19535 std::thread::yield_now();
19536 }
19537 assert!(
19538 matches!(connection.core.state, H2State::ContinuationHeader(_)),
19539 "stream 1's HEADERS frame must be accepted and await CONTINUATION: {:?}",
19540 connection.core.state
19541 );
19542 assert!(
19543 !connection.core.stream_table.is_empty(),
19544 "a non-refused HEADERS frame must create stream 1"
19545 );
19546
19547 // Exactly #1423's trigger: the connection is draining AND the
19548 // frontend readiness reports HUP, landing squarely mid-reassembly.
19549 // A peer can legitimately produce this by sending TCP FIN together
19550 // with (or shortly after) the last CONTINUATION frame it manages to
19551 // get out, observed by edge-triggered epoll as one combined event.
19552 connection.core.drain.__test_set_draining();
19553 connection.core.readiness.event.insert(Ready::HUP);
19554
19555 // `flush_pending_control_frames`'s first, HUP-gated stage fires
19556 // here.
19557 connection.writable(&mut context, EndpointClient(&mut router));
19558
19559 // Real epoll delivers HUP once; un-signal it so the reads below
19560 // reflect the peer's normal traffic rather than a second synthetic
19561 // HUP this test injected by hand.
19562 connection.core.readiness.event.remove(Ready::HUP);
19563
19564 // Now the CONTINUATION frame completing the block arrives — still
19565 // fully deliverable, exactly as it would be if the peer's FIN and
19566 // its last frame arrived close together but the block itself was
19567 // sent in full.
19568 let mut continuation_frame = Vec::with_capacity(9 + second_half.len());
19569 continuation_frame.extend_from_slice(&(second_half.len() as u32).to_be_bytes()[1..]);
19570 continuation_frame.push(9); // CONTINUATION
19571 continuation_frame.push(parser::FLAG_END_HEADERS);
19572 continuation_frame.extend_from_slice(&1u32.to_be_bytes());
19573 continuation_frame.extend_from_slice(second_half);
19574 peer.write_all(&continuation_frame)
19575 .expect("loopback write must complete");
19576 peer.flush().expect("loopback flush must complete");
19577
19578 for _ in 0..64 {
19579 connection.readable(&mut context, EndpointClient(&mut router));
19580 if !matches!(
19581 connection.core.state,
19582 H2State::ContinuationHeader(_) | H2State::ContinuationFrame(_)
19583 ) {
19584 break;
19585 }
19586 std::thread::yield_now();
19587 }
19588
19589 assert!(
19590 !matches!(connection.core.state, H2State::Error | H2State::GoAway),
19591 "a HUP-while-draining event landing mid-reassembly must not corrupt \
19592 a legitimate in-progress header block into a connection error: {:?}",
19593 connection.core.state
19594 );
19595
19596 // The peer's next block references the dynamic entry its encoder
19597 // added while encoding stream 1's request.
19598 let next_block = peer_encoder.encode([(&b"x-sozu-marker"[..], &b"legitimate-value"[..])]);
19599 assert_eq!(
19600 next_block.len(),
19601 1,
19602 "the peer must now reference its dynamic entry, not re-send a literal"
19603 );
19604
19605 let mut decoded = Vec::new();
19606 let status = connection
19607 .core
19608 .hpack
19609 .decoder_mut()
19610 .decode_with_cb(&next_block, |k, v| {
19611 decoded.push((k.into_owned(), v.into_owned()));
19612 });
19613
19614 assert!(
19615 status.is_ok(),
19616 "the connection decoder must still resolve the peer's dynamic table \
19617 after a HUP-while-draining event during header-block reassembly, \
19618 got {status:?}"
19619 );
19620 assert_eq!(
19621 decoded,
19622 vec![(b"x-sozu-marker".to_vec(), b"legitimate-value".to_vec())],
19623 "stream 1's field block must have been decoded byte-for-byte, not \
19624 clobbered by the HUP-while-draining stage of \
19625 flush_pending_control_frames"
19626 );
19627 }
19628
19629 /// Regression test for B1 (review of `e1c3c2fb`): `handle_headers_frame`'s
19630 /// RFC 7540 §5.3.1 self-dependency early return — `reset_stream` +
19631 /// `remove_dead_stream` when a stream's own PRIORITY depends on itself —
19632 /// used to skip retiring `self.header_reassembly` before returning. When
19633 /// the aborted stream's block had gone through CONTINUATION reassembly
19634 /// (`header_reassembly.is_in_progress() == true` at abort time), the
19635 /// flag leaked into the very NEXT HEADERS frame processed on this
19636 /// connection: that frame's own "read from `zero.storage`" fast path
19637 /// was silently skipped in favour of the aborted stream's stale,
19638 /// already-discarded bytes — corrupting an entirely unrelated stream's
19639 /// decoded request. This reproduces in BOTH debug and release builds:
19640 /// nothing on this path was ever a `debug_assert!`.
19641 ///
19642 /// To SEE THIS RED on `e1c3c2fb` (before this fix): stream 1's block
19643 /// completes reassembly, its RFC 7540 PRIORITY self-dependency
19644 /// (`stream_dependency.stream_id == 1`) aborts it via `reset_stream`
19645 /// without retiring the accumulator, and
19646 /// `header_reassembly.is_in_progress()` stays `true`. Stream 3's own,
19647 /// unrelated, single-frame request is then decoded from stream 1's
19648 /// stale fragment instead of its own bytes: the `:path` pseudo-header
19649 /// this test asserts on is corrupted rather than
19650 /// `"/stream-3-legitimate"`.
19651 #[test]
19652 fn a_priority_self_dependency_reset_does_not_leak_the_reassembly_accumulator() {
19653 use std::io::Write;
19654
19655 let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 20, 16_384)));
19656 let (mut connection, mut peer) = test_h2_connection(&pool, None);
19657 let mut context = test_context(&pool);
19658 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
19659
19660 connection.core.state = H2State::Header;
19661 connection
19662 .core
19663 .stream_table
19664 .set_expect_read(Some((H2StreamId::Zero, 9)));
19665
19666 // Stream 1: a legitimate multi-frame header block — split across
19667 // HEADERS + CONTINUATION, exactly like the sibling reassembly tests
19668 // — but the FIRST frame also carries an RFC 7540 PRIORITY field
19669 // whose stream dependency is stream 1 itself (RFC 7540 §5.3.1: a
19670 // stream cannot depend on itself).
19671 let mut peer_encoder = crate::protocol::mux::hpack::Encoder::new();
19672 let field_block = peer_encoder.encode([
19673 (&b":method"[..], &b"GET"[..]),
19674 (&b":scheme"[..], &b"https"[..]),
19675 (&b":authority"[..], &b"example.com"[..]),
19676 (&b":path"[..], &b"/stream-1-aborted"[..]),
19677 ]);
19678 assert!(
19679 field_block.len() > 10,
19680 "the probe block must be large enough to split meaningfully"
19681 );
19682 let split = field_block.len() / 2;
19683 let (first_half, second_half) = field_block.split_at(split);
19684
19685 // HEADERS, PRIORITY (self-dependency), NOT END_HEADERS.
19686 let mut priority_prefix = Vec::with_capacity(5);
19687 priority_prefix.extend_from_slice(&1u32.to_be_bytes()); // stream_dependency = 1 (self)
19688 priority_prefix.push(15); // weight
19689 let mut headers_frame = Vec::with_capacity(9 + priority_prefix.len() + first_half.len());
19690 headers_frame.extend_from_slice(
19691 &((priority_prefix.len() + first_half.len()) as u32).to_be_bytes()[1..],
19692 );
19693 headers_frame.push(1); // HEADERS
19694 headers_frame.push(parser::FLAG_PRIORITY);
19695 headers_frame.extend_from_slice(&1u32.to_be_bytes());
19696 headers_frame.extend_from_slice(&priority_prefix);
19697 headers_frame.extend_from_slice(first_half);
19698 peer.write_all(&headers_frame)
19699 .expect("loopback write must complete");
19700 peer.flush().expect("loopback flush must complete");
19701
19702 for _ in 0..64 {
19703 connection.readable(&mut context, EndpointClient(&mut router));
19704 if matches!(connection.core.state, H2State::ContinuationHeader(_)) {
19705 break;
19706 }
19707 std::thread::yield_now();
19708 }
19709 assert!(
19710 matches!(connection.core.state, H2State::ContinuationHeader(_)),
19711 "stream 1's HEADERS frame must be accepted and await CONTINUATION: {:?}",
19712 connection.core.state
19713 );
19714
19715 // Complete the block — this re-enters `handle_headers_frame`, which
19716 // detects the self-dependency and aborts stream 1.
19717 let mut continuation_frame = Vec::with_capacity(9 + second_half.len());
19718 continuation_frame.extend_from_slice(&(second_half.len() as u32).to_be_bytes()[1..]);
19719 continuation_frame.push(9); // CONTINUATION
19720 continuation_frame.push(parser::FLAG_END_HEADERS);
19721 continuation_frame.extend_from_slice(&1u32.to_be_bytes());
19722 continuation_frame.extend_from_slice(second_half);
19723 peer.write_all(&continuation_frame)
19724 .expect("loopback write must complete");
19725 peer.flush().expect("loopback flush must complete");
19726
19727 for _ in 0..64 {
19728 connection.readable(&mut context, EndpointClient(&mut router));
19729 if connection.core.stream_table.get(1).is_none() {
19730 break;
19731 }
19732 std::thread::yield_now();
19733 }
19734
19735 // Premise: stream 1 really was torn down for self-dependency.
19736 assert!(
19737 connection.core.stream_table.get(1).is_none(),
19738 "the self-dependent stream must have been torn down"
19739 );
19740
19741 // The direct proof of the fix: the accumulator must be idle again
19742 // before any other stream's HEADERS frame is processed. `h2.rs`'s
19743 // own test module can reach the private field directly — no
19744 // test-only accessor needed.
19745 assert!(
19746 !connection.core.header_reassembly.is_in_progress(),
19747 "aborting a self-dependent stream mid-reassembly must retire the \
19748 accumulator, not leave it `is_in_progress() == true` for the \
19749 next HEADERS frame on this connection"
19750 );
19751
19752 // Stream 3: an entirely unrelated, single-frame, complete request.
19753 let stream3_block = peer_encoder.encode([
19754 (&b":method"[..], &b"GET"[..]),
19755 (&b":scheme"[..], &b"https"[..]),
19756 (&b":authority"[..], &b"example.com"[..]),
19757 (&b":path"[..], &b"/stream-3-legitimate"[..]),
19758 ]);
19759 let mut stream3_frame = Vec::with_capacity(9 + stream3_block.len());
19760 stream3_frame.extend_from_slice(&(stream3_block.len() as u32).to_be_bytes()[1..]);
19761 stream3_frame.push(1); // HEADERS
19762 stream3_frame.push(parser::FLAG_END_STREAM | parser::FLAG_END_HEADERS);
19763 stream3_frame.extend_from_slice(&3u32.to_be_bytes());
19764 stream3_frame.extend_from_slice(&stream3_block);
19765 peer.write_all(&stream3_frame)
19766 .expect("loopback write must complete");
19767 peer.flush().expect("loopback flush must complete");
19768
19769 // Wait until the connection is back at `Header`, waiting on the
19770 // NEXT frame's 9-byte header — not merely until the stream slot
19771 // exists, which `create_stream` populates as soon as the frame
19772 // HEADER is parsed, well before its payload is read and decoded.
19773 for _ in 0..64 {
19774 connection.readable(&mut context, EndpointClient(&mut router));
19775 if matches!(connection.core.state, H2State::Header)
19776 && matches!(
19777 connection.core.stream_table.expect_read(),
19778 Some((H2StreamId::Zero, 9))
19779 )
19780 {
19781 break;
19782 }
19783 std::thread::yield_now();
19784 }
19785
19786 let stream3_gid = connection
19787 .core
19788 .stream_table
19789 .get(3)
19790 .expect("stream 3's HEADERS frame must create a stream");
19791 assert_eq!(
19792 context.streams[stream3_gid].context.path.as_deref(),
19793 Some("/stream-3-legitimate"),
19794 "stream 3 must decode its OWN bytes, not stream 1's stale, \
19795 aborted reassembly fragment"
19796 );
19797 }
19798
19799 /// Regression test for B2 (review of `e1c3c2fb`): `flush_pending_control_frames`'s
19800 /// `frontend_hung_up_while_draining()` stage used to clear `zero.storage`
19801 /// unconditionally, on the premise that `Ready::HUP` means "no further
19802 /// bytes can ever arrive". That premise is false: `Ready::HUP` is
19803 /// `is_read_closed() || is_write_closed()` (`command/src/ready.rs`), and
19804 /// mio documents `is_read_closed()` as also true on a TCP half-close —
19805 /// a FIN with data the peer already sent still sitting, unread, in the
19806 /// kernel receive queue. A CONTINUATION frame's payload split across two
19807 /// TCP segments can have its FIRST segment already read into
19808 /// `zero.storage` (a genuine partial `socket_read()`, not stale bytes
19809 /// from a completed pass) when the readiness event batches HUP together
19810 /// with that first segment; `drive_frontend_shutdown_io` (`mod.rs`)
19811 /// force-calls `readable()` for H2 on every `shutting_down()` poll, so
19812 /// this is the ordinary soft-stop path, not a contrived corner case.
19813 ///
19814 /// To SEE THIS RED on `e1c3c2fb` (before this fix): the partial first
19815 /// segment is wiped by the unconditional clear; the second segment then
19816 /// lands at the wrong (reset-to-zero) offset once appended, and the
19817 /// connection's HPACK decoder desyncs from the peer's encoder — the
19818 /// same silent-wrong-values failure mode as #1397/#1401/#1423.
19819 #[test]
19820 fn a_continuation_frame_split_by_tcp_segmentation_survives_a_hup_while_draining() {
19821 use std::io::Write;
19822
19823 let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 20, 16_384)));
19824 let (mut connection, mut peer) = test_h2_connection(&pool, None);
19825 let mut context = test_context(&pool);
19826 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
19827
19828 connection.core.state = H2State::Header;
19829 connection
19830 .core
19831 .stream_table
19832 .set_expect_read(Some((H2StreamId::Zero, 9)));
19833
19834 let mut peer_encoder = crate::protocol::mux::hpack::Encoder::new();
19835 let field_block = peer_encoder.encode([
19836 (&b":method"[..], &b"GET"[..]),
19837 (&b":scheme"[..], &b"https"[..]),
19838 (&b":authority"[..], &b"example.com"[..]),
19839 (&b":path"[..], &b"/tcp-segmented"[..]),
19840 (&b"x-sozu-marker"[..], &b"legitimate-value"[..]),
19841 ]);
19842 assert!(
19843 field_block.len() > 40,
19844 "the probe block must be large enough to split meaningfully"
19845 );
19846 let split = field_block.len() / 2;
19847 let (first_half, continuation_payload) = field_block.split_at(split);
19848 assert!(
19849 continuation_payload.len() > 10,
19850 "the CONTINUATION frame's own payload must be large enough to \
19851 split into two TCP segments"
19852 );
19853 let payload_split = continuation_payload.len() / 2;
19854 let (payload_segment_1, payload_segment_2) = continuation_payload.split_at(payload_split);
19855
19856 // HEADERS, END_STREAM but NOT END_HEADERS, stream 1.
19857 let mut headers_frame = Vec::with_capacity(9 + first_half.len());
19858 headers_frame.extend_from_slice(&(first_half.len() as u32).to_be_bytes()[1..]);
19859 headers_frame.push(1); // HEADERS
19860 headers_frame.push(parser::FLAG_END_STREAM);
19861 headers_frame.extend_from_slice(&1u32.to_be_bytes());
19862 headers_frame.extend_from_slice(first_half);
19863 peer.write_all(&headers_frame)
19864 .expect("loopback write must complete");
19865 peer.flush().expect("loopback flush must complete");
19866
19867 for _ in 0..64 {
19868 connection.readable(&mut context, EndpointClient(&mut router));
19869 if matches!(connection.core.state, H2State::ContinuationHeader(_)) {
19870 break;
19871 }
19872 std::thread::yield_now();
19873 }
19874 assert!(
19875 matches!(connection.core.state, H2State::ContinuationHeader(_)),
19876 "stream 1's HEADERS frame must be accepted and await CONTINUATION: {:?}",
19877 connection.core.state
19878 );
19879
19880 // The CONTINUATION frame header, announcing the FULL payload
19881 // length, followed by only its FIRST TCP segment.
19882 let mut continuation_header_and_segment_1 = Vec::with_capacity(9 + payload_segment_1.len());
19883 continuation_header_and_segment_1
19884 .extend_from_slice(&(continuation_payload.len() as u32).to_be_bytes()[1..]);
19885 continuation_header_and_segment_1.push(9); // CONTINUATION
19886 continuation_header_and_segment_1.push(parser::FLAG_END_HEADERS);
19887 continuation_header_and_segment_1.extend_from_slice(&1u32.to_be_bytes());
19888 continuation_header_and_segment_1.extend_from_slice(payload_segment_1);
19889 peer.write_all(&continuation_header_and_segment_1)
19890 .expect("loopback write must complete");
19891 peer.flush().expect("loopback flush must complete");
19892
19893 // Drive reads until the connection has consumed exactly the first
19894 // segment and is waiting on the rest of THIS SAME CONTINUATION
19895 // frame's payload — a genuine partial `socket_read()`, proven by
19896 // `expect_read` still wanting the remainder.
19897 for _ in 0..64 {
19898 connection.readable(&mut context, EndpointClient(&mut router));
19899 if matches!(connection.core.state, H2State::ContinuationFrame(_))
19900 && matches!(
19901 connection.core.stream_table.expect_read(),
19902 Some((H2StreamId::Zero, remaining)) if remaining == payload_segment_2.len()
19903 )
19904 {
19905 break;
19906 }
19907 std::thread::yield_now();
19908 }
19909 assert!(
19910 matches!(connection.core.state, H2State::ContinuationFrame(_)),
19911 "the CONTINUATION frame must be mid-payload, waiting on its \
19912 second TCP segment: {:?}",
19913 connection.core.state
19914 );
19915 assert_eq!(
19916 connection.core.stream_table.expect_read(),
19917 Some((H2StreamId::Zero, payload_segment_2.len())),
19918 "expect_read must want exactly the remaining bytes of this \
19919 frame's own payload — proof this is a genuine partial read, \
19920 not an inter-frame gap"
19921 );
19922
19923 // Exactly B2's trigger: the connection is draining AND the frontend
19924 // readiness reports HUP, landing squarely mid-payload-read — the
19925 // shape a TCP half-close produces (mio's `is_read_closed()` can be
19926 // true with data the peer already sent still unread).
19927 connection.core.drain.__test_set_draining();
19928 connection.core.readiness.event.insert(Ready::HUP);
19929 connection.writable(&mut context, EndpointClient(&mut router));
19930 connection.core.readiness.event.remove(Ready::HUP);
19931
19932 // Now the second TCP segment, completing the CONTINUATION frame's
19933 // payload, arrives.
19934 peer.write_all(payload_segment_2)
19935 .expect("loopback write must complete");
19936 peer.flush().expect("loopback flush must complete");
19937
19938 for _ in 0..64 {
19939 connection.readable(&mut context, EndpointClient(&mut router));
19940 if !matches!(
19941 connection.core.state,
19942 H2State::ContinuationHeader(_) | H2State::ContinuationFrame(_)
19943 ) {
19944 break;
19945 }
19946 std::thread::yield_now();
19947 }
19948
19949 assert!(
19950 !matches!(connection.core.state, H2State::Error | H2State::GoAway),
19951 "a HUP-while-draining event landing mid-TCP-segment must not \
19952 corrupt a legitimate in-progress header block into a \
19953 connection error: {:?}",
19954 connection.core.state
19955 );
19956
19957 let next_block = peer_encoder.encode([(&b"x-sozu-marker"[..], &b"legitimate-value"[..])]);
19958 assert_eq!(
19959 next_block.len(),
19960 1,
19961 "the peer must now reference its dynamic entry, not re-send a literal"
19962 );
19963
19964 let mut decoded = Vec::new();
19965 let status = connection
19966 .core
19967 .hpack
19968 .decoder_mut()
19969 .decode_with_cb(&next_block, |k, v| {
19970 decoded.push((k.into_owned(), v.into_owned()));
19971 });
19972
19973 assert!(
19974 status.is_ok(),
19975 "the connection decoder must still resolve the peer's dynamic \
19976 table after a HUP-while-draining event landing mid-TCP-segment, \
19977 got {status:?}"
19978 );
19979 assert_eq!(
19980 decoded,
19981 vec![(b"x-sozu-marker".to_vec(), b"legitimate-value".to_vec())],
19982 "stream 1's field block must have been decoded byte-for-byte, \
19983 not clobbered by a HUP-while-draining event landing between \
19984 two TCP segments of the same CONTINUATION frame"
19985 );
19986 }
19987
19988 // ── The HPACK encoder across one multi-stream write pass ────────────
19989 //
19990 // `converter.rs`'s own unit tests drive ONE `H2BlockConverter` over ONE
19991 // kawa. Nothing there can see a property that only exists across the
19992 // streams of a single `write_streams` pass, and those are precisely the
19993 // two the per-`kawa.prepare` converter scoping has to preserve: the
19994 // RFC 7541 §6.3 size-update prefix belongs to the first header block of
19995 // the pass and to no other, and every block of the pass is encoded
19996 // against the SAME encoder dynamic table.
19997
19998 /// Split a captured H2 byte stream into its HEADERS frame payloads,
19999 /// each paired with the stream it belongs to, in wire order. DATA and
20000 /// control frames are skipped; a truncated capture is a test bug and
20001 /// asserts rather than silently returning a short list.
20002 fn headers_blocks(wire: &[u8]) -> Vec<(StreamId, Vec<u8>)> {
20003 let mut blocks = Vec::new();
20004 let mut offset = 0usize;
20005 while offset + parser::FRAME_HEADER_SIZE <= wire.len() {
20006 let payload_len = ((wire[offset] as usize) << 16)
20007 | ((wire[offset + 1] as usize) << 8)
20008 | wire[offset + 2] as usize;
20009 let frame_type = wire[offset + 3];
20010 let stream_id = u32::from_be_bytes([
20011 wire[offset + 5],
20012 wire[offset + 6],
20013 wire[offset + 7],
20014 wire[offset + 8],
20015 ]) & 0x7fff_ffff;
20016 let start = offset + parser::FRAME_HEADER_SIZE;
20017 let end = start + payload_len;
20018 assert!(
20019 end <= wire.len(),
20020 "captured frame must be complete: {payload_len} payload bytes \
20021 declared at offset {offset} of a {} byte capture",
20022 wire.len()
20023 );
20024 if frame_type == 1 {
20025 blocks.push((stream_id, wire[start..end].to_vec()));
20026 }
20027 offset = end;
20028 }
20029 blocks
20030 }
20031
20032 /// `true` when `block` opens with an RFC 7541 §6.3 dynamic-table-size
20033 /// update. The representation prefixes partition the first byte with no
20034 /// overlap — `1xxxxxxx` indexed, `01xxxxxx` literal-with-indexing,
20035 /// `0001xxxx` literal-never-indexed, `0000xxxx` literal-without-indexing
20036 /// — so the `001xxxxx` pattern identifies the update on its own,
20037 /// whatever integer follows it.
20038 fn starts_with_size_update(block: &[u8]) -> bool {
20039 block.first().is_some_and(|b| b & 0xE0 == 0x20)
20040 }
20041
20042 /// Drive exactly ONE `write_streams` pass that emits a response header
20043 /// block on every `stream_ids` entry, and return those blocks in the
20044 /// order they reached the wire together with whatever size-update the
20045 /// connection still has queued afterwards.
20046 ///
20047 /// Each stream is opened by a real HEADERS frame from the peer and then
20048 /// filled with sozu's own 404 default answer through
20049 /// `answers::set_default_answer`, the same chokepoint the routing layer
20050 /// uses — so the blocks captured here are the ones production encodes.
20051 ///
20052 /// `pending_table_size_update` is installed the way `handle_settings_frame`
20053 /// installs it: the encoder cap and the queued signal move together.
20054 fn drive_one_write_pass(
20055 stream_ids: &[StreamId],
20056 pending_table_size_update: Option<u32>,
20057 ) -> (Vec<(StreamId, Vec<u8>)>, Option<u32>) {
20058 use std::io::{Read, Write};
20059
20060 let pool = Rc::new(RefCell::new(Pool::with_capacity(8, 40, 16_384)));
20061 let (mut connection, mut peer) = test_h2_connection(&pool, None);
20062 let mut context = test_context(&pool);
20063 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
20064
20065 connection.core.state = H2State::Header;
20066 connection
20067 .core
20068 .stream_table
20069 .set_expect_read(Some((H2StreamId::Zero, 9)));
20070
20071 let mut peer_encoder = crate::protocol::mux::hpack::Encoder::new();
20072 for &stream_id in stream_ids {
20073 let field_block = peer_encoder.encode([
20074 (&b":method"[..], &b"GET"[..]),
20075 (&b":scheme"[..], &b"https"[..]),
20076 (&b":authority"[..], &b"example.com"[..]),
20077 (&b":path"[..], &b"/write-pass"[..]),
20078 ]);
20079 let mut frame = Vec::with_capacity(parser::FRAME_HEADER_SIZE + field_block.len());
20080 frame.extend_from_slice(&(field_block.len() as u32).to_be_bytes()[1..]);
20081 frame.push(1); // HEADERS
20082 frame.push(parser::FLAG_END_STREAM | parser::FLAG_END_HEADERS);
20083 frame.extend_from_slice(&stream_id.to_be_bytes());
20084 frame.extend_from_slice(&field_block);
20085 peer.write_all(&frame)
20086 .expect("loopback write must complete");
20087 peer.flush().expect("loopback flush must complete");
20088 for _ in 0..64 {
20089 connection.readable(&mut context, EndpointClient(&mut router));
20090 if connection
20091 .core
20092 .stream_table
20093 .streams()
20094 .contains_key(&stream_id)
20095 {
20096 break;
20097 }
20098 std::thread::yield_now();
20099 }
20100 assert!(
20101 connection
20102 .core
20103 .stream_table
20104 .streams()
20105 .contains_key(&stream_id),
20106 "stream {stream_id} must be accepted before the write pass"
20107 );
20108 }
20109
20110 let answers = context.listener.borrow().get_answers().clone();
20111 let open: Vec<GlobalStreamId> = connection
20112 .core
20113 .stream_table
20114 .streams()
20115 .values()
20116 .copied()
20117 .collect();
20118 for global_stream_id in open {
20119 crate::protocol::mux::answers::set_default_answer(
20120 &mut context.streams[global_stream_id],
20121 &mut connection.core.readiness,
20122 404,
20123 &answers.borrow(),
20124 );
20125 }
20126
20127 // Discard whatever the handshake already put on the wire, so the
20128 // capture below holds this write pass and nothing else.
20129 let mut discard = Vec::new();
20130 let _ = peer.read_to_end(&mut discard);
20131
20132 if let Some(size) = pending_table_size_update {
20133 connection
20134 .core
20135 .hpack
20136 .set_encoder_max_table_size(size as usize);
20137 }
20138 connection.core.pending_table_size_update = pending_table_size_update;
20139 connection.writable(&mut context, EndpointClient(&mut router));
20140
20141 let mut wire = Vec::new();
20142 let _ = peer.read_to_end(&mut wire);
20143 (
20144 headers_blocks(&wire),
20145 connection.core.pending_table_size_update,
20146 )
20147 }
20148
20149 /// Two properties of a `write_streams` pass, both invisible to any
20150 /// single-stream test, pinned on the bytes two streams actually receive:
20151 ///
20152 /// 1. the RFC 7541 §6.3 dynamic-table-size-update prefix is written to
20153 /// the FIRST header block of the pass and to no other, and the
20154 /// connection's mirror is cleared only because a block carried it;
20155 /// 2. both blocks are encoded against ONE encoder dynamic table, so a
20156 /// peer replaying them in order with a single decoder stays in sync —
20157 /// and a decoder that never saw the first block CANNOT read the
20158 /// second, which is the negative half that makes assertion 2
20159 /// discriminating rather than vacuous.
20160 ///
20161 /// Scoping the `H2BlockConverter` to a single `kawa.prepare` call is
20162 /// what puts both at risk: the converter no longer spans the per-stream
20163 /// loop, so the signal and the encoder now cross from one stream to the
20164 /// next through [`converter::H2ConverterPass`] rather than through one
20165 /// long-lived struct.
20166 ///
20167 /// TO SEE THIS RED, break either half:
20168 /// (a) delete `self.pending_table_size_update = converter.pending_table_size_update;`
20169 /// from `converter::H2ConverterPass::reclaim` — every block of the
20170 /// pass then re-emits the prefix, and the second assertion fails with
20171 /// `only the FIRST header block of a pass carries the size update;
20172 /// stream 3 opened with 0x3f`;
20173 /// (b) pass a fresh `crate::protocol::mux::hpack::Encoder::new()` to `pass.converter(..)`
20174 /// in `write_streams` instead of `self.hpack.encoder_mut()` — every
20175 /// block becomes self-contained and the LAST assertion fails with
20176 /// `the second block must depend on the dynamic table the first one
20177 /// built`.
20178 #[test]
20179 fn one_write_pass_prefixes_its_first_header_block_only_and_shares_one_encoder() {
20180 // 4096 is the HPACK default table size, so `[0x3f, 0xe1, 0x1f]` below
20181 // is the canonical §6.3 encoding: prefix bits `001`, a 5-bit prefix
20182 // integer saturated to 31, then 4096 - 31 = 4065 as two continuation
20183 // octets (0x61 | 0x80, 0x1f). Written out rather than produced with
20184 // the same encoder the production path uses, so the test cannot
20185 // agree with a broken encoder.
20186 const TABLE_SIZE: u32 = 4096;
20187 const SIZE_UPDATE_PREFIX: [u8; 3] = [0x3f, 0xe1, 0x1f];
20188
20189 let (blocks, residual) = drive_one_write_pass(&[1, 3], Some(TABLE_SIZE));
20190
20191 assert_eq!(
20192 blocks.iter().map(|(id, _)| *id).collect::<Vec<_>>(),
20193 vec![1, 3],
20194 "one pass must emit one header block per open stream, in the \
20195 priority order (equal urgency, ascending stream id)"
20196 );
20197
20198 assert_eq!(
20199 &blocks[0].1[..SIZE_UPDATE_PREFIX.len()],
20200 &SIZE_UPDATE_PREFIX,
20201 "the first header block of the pass must open with the \
20202 dynamic-table-size-update for {TABLE_SIZE}"
20203 );
20204 assert!(
20205 !starts_with_size_update(&blocks[1].1),
20206 "only the FIRST header block of a pass carries the size update; \
20207 stream {} opened with {:#04x}",
20208 blocks[1].0,
20209 blocks[1].1[0]
20210 );
20211 assert_eq!(
20212 residual, None,
20213 "the connection clears its mirror once a block carried the signal"
20214 );
20215
20216 // Positive half: the peer's single decoder replays the pass in order.
20217 let mut peer_decoder = crate::protocol::mux::hpack::Decoder::new();
20218 peer_decoder.set_max_allowed_table_size(TABLE_SIZE as usize);
20219 for (stream_id, block) in &blocks {
20220 let mut decoded = Vec::new();
20221 let status = peer_decoder.decode_with_cb(block, |key, value| {
20222 decoded.push((key.into_owned(), value.into_owned()));
20223 });
20224 assert!(
20225 status.is_ok(),
20226 "a peer replaying the pass in order must decode stream \
20227 {stream_id}'s block: {status:?}"
20228 );
20229 assert!(
20230 decoded.contains(&(b":status".to_vec(), b"404".to_vec())),
20231 "stream {stream_id}'s block must carry the 404 status line, \
20232 got {decoded:?}"
20233 );
20234 }
20235
20236 // Negative half: the second block is not self-contained. It indexes
20237 // dynamic-table entries only the first block created, so a decoder
20238 // that never saw the first block cannot read it. Without this, a
20239 // per-stream encoder would satisfy every assertion above.
20240 let mut fresh_decoder = crate::protocol::mux::hpack::Decoder::new();
20241 fresh_decoder.set_max_allowed_table_size(TABLE_SIZE as usize);
20242 let status = fresh_decoder.decode_with_cb(&blocks[1].1, |_, _| {});
20243 assert!(
20244 status.is_err(),
20245 "the second block must depend on the dynamic table the first one \
20246 built — a decoder that never saw the first block must fail on \
20247 it, otherwise the two blocks came from two encoders"
20248 );
20249 }
20250
20251 // ── RFC 9113 §3.4 client connection preface, read-fragmentation axis ──
20252 //
20253 // `Connection::new_h2_server` arms `expect_read` with
20254 // `CLIENT_PREFACE_SIZE` (the 24-octet magic string plus a 9-octet
20255 // SETTINGS frame header). When a read comes back SHORT of that request,
20256 // the short-read branch of `H2Shell::readable` runs an early guard
20257 // so a client that is plainly not speaking H2 is dropped without waiting
20258 // for the rest of the window.
20259 //
20260 // Nothing about a TCP segment or a TLS record guarantees it lands on the
20261 // 24-octet boundary, so that guard has to tolerate EVERY split of a
20262 // byte-perfect preface while still refusing a wrong one.
20263
20264 /// The whole accumulated window, at every chunk size, is the axis that
20265 /// matters here — not one hand-picked split. The failure this pins is
20266 /// invisible at exactly 24 (the window never grows past the magic
20267 /// string, so a prefix test against it still answers correctly) and at
20268 /// exactly 33 (the request is filled, so the short-read branch is never
20269 /// entered at all) — the two sizes a hand-written test is most likely to
20270 /// pick. So the sweep collects every failing size instead of asserting
20271 /// inside the loop, which would stop at the first one and hide the band.
20272 ///
20273 /// To SEE THIS RED: in the `H2State::ClientPreface` arm of
20274 /// `H2Shell::readable`'s short-read branch, compare the whole
20275 /// accumulated window instead of the part the constant can cover —
20276 /// `if !pri.starts_with(i)` in place of
20277 /// `if !pri.starts_with(&i[..i.len().min(pri.len())])`. `starts_with`
20278 /// asks whether the window is a prefix OF the 24-octet constant, so it
20279 /// answers false for every window longer than 24 whatever the window
20280 /// holds, and this reports `[1, 25, 26, 27, 28, 29, 30, 31, 32]`.
20281 #[test]
20282 fn a_byte_perfect_client_preface_survives_every_read_fragmentation() {
20283 use std::io::Write;
20284
20285 // Built from the wire bytes, not from `serializer::H2_PRI`, so this
20286 // also pins the constant's value rather than agreeing with whatever
20287 // it happens to hold.
20288 let mut wire = Vec::with_capacity(CLIENT_PREFACE_SIZE);
20289 wire.extend_from_slice(b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n");
20290 // SETTINGS, empty payload, no flags, stream 0 — RFC 9113 §6.5.
20291 wire.extend_from_slice(&[0, 0, 0, 4, 0, 0, 0, 0, 0]);
20292 assert_eq!(
20293 wire.len(),
20294 CLIENT_PREFACE_SIZE,
20295 "the probe must be exactly the window the server asks for"
20296 );
20297
20298 let mut disconnected = Vec::new();
20299
20300 for chunk in 1..=40usize {
20301 let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16_384)));
20302 let (mut connection, mut peer) = test_h2_connection(&pool, None);
20303 let mut context = test_context(&pool);
20304 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
20305
20306 let mut delivered = 0usize;
20307 let mut closed = false;
20308
20309 'delivery: for piece in wire.chunks(chunk) {
20310 peer.write_all(piece).expect("loopback write must complete");
20311 peer.flush().expect("loopback flush must complete");
20312 delivered += piece.len();
20313
20314 // Drive until THIS piece has been consumed before writing the
20315 // next one. Two loopback writes the connection has not read
20316 // between coalesce in the kernel receive queue, which would
20317 // hand `socket_read` a full 33-octet window, take the
20318 // not-short path, and never reach the guard under test.
20319 for _ in 0..64 {
20320 if !matches!(connection.core.state, H2State::ClientPreface) {
20321 break 'delivery;
20322 }
20323 if matches!(
20324 connection.readable(&mut context, EndpointClient(&mut router)),
20325 MuxResult::CloseSession
20326 ) {
20327 closed = true;
20328 break 'delivery;
20329 }
20330 if connection.core.stream_table.expect_read()
20331 == Some((H2StreamId::Zero, CLIENT_PREFACE_SIZE - delivered))
20332 {
20333 break;
20334 }
20335 std::thread::yield_now();
20336 }
20337 }
20338
20339 if closed
20340 || matches!(
20341 connection.core.state,
20342 H2State::ClientPreface | H2State::Error | H2State::GoAway
20343 )
20344 {
20345 disconnected.push(chunk);
20346 }
20347 }
20348
20349 assert!(
20350 disconnected.is_empty(),
20351 "a byte-perfect client preface must be accepted however the \
20352 reads are fragmented; these chunk sizes were disconnected \
20353 instead: {disconnected:?}"
20354 );
20355 }
20356
20357 /// The other half of the same guard: clipping the comparison to the part
20358 /// the magic string can cover must not blunt it. A window that is wrong
20359 /// INSIDE the first 24 octets is still refused while the request is
20360 /// unfilled — which is the only moment the early guard can act, since
20361 /// `parser::preface` in the `H2State::ClientPreface` arm is reached only
20362 /// once the whole `CLIENT_PREFACE_SIZE` window has been read.
20363 ///
20364 /// Corrupting the FIRST and the LAST octet of the magic string proves
20365 /// the comparison still spans all 24, and sweeping the window across the
20366 /// 25..=32 band proves the fix did not turn the band into a hole that
20367 /// lets a non-H2 client linger.
20368 ///
20369 /// To SEE THIS RED: drop the guard's `!`, or clip the comparison to
20370 /// fewer octets than the magic string holds — e.g.
20371 /// `&i[..i.len().min(pri.len() - 1)]`, which stops refusing the
20372 /// `corrupt_at = 23` cases.
20373 #[test]
20374 fn an_invalid_client_preface_is_still_refused_before_the_window_is_filled() {
20375 use std::io::Write;
20376
20377 let mut survivors = Vec::new();
20378
20379 for corrupt_at in [0usize, 23] {
20380 for window in (corrupt_at + 1)..CLIENT_PREFACE_SIZE {
20381 let mut wire = Vec::with_capacity(CLIENT_PREFACE_SIZE);
20382 wire.extend_from_slice(b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n");
20383 wire.extend_from_slice(&[0, 0, 0, 4, 0, 0, 0, 0, 0]);
20384 wire[corrupt_at] ^= 0xff;
20385 wire.truncate(window);
20386
20387 let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16_384)));
20388 let (mut connection, mut peer) = test_h2_connection(&pool, None);
20389 let mut context = test_context(&pool);
20390 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
20391
20392 peer.write_all(&wire).expect("loopback write must complete");
20393 peer.flush().expect("loopback flush must complete");
20394
20395 // The window is short of `CLIENT_PREFACE_SIZE`, so the
20396 // connection never leaves `H2State::ClientPreface` on its
20397 // own: whatever refuses it here is the early guard.
20398 for _ in 0..64 {
20399 if matches!(
20400 connection.readable(&mut context, EndpointClient(&mut router)),
20401 MuxResult::CloseSession
20402 ) {
20403 break;
20404 }
20405 std::thread::yield_now();
20406 }
20407
20408 if !matches!(connection.core.state, H2State::Error) {
20409 survivors.push((corrupt_at, window));
20410 }
20411 }
20412 }
20413
20414 assert!(
20415 survivors.is_empty(),
20416 "a preface wrong inside its first 24 octets must be refused \
20417 before the request is filled; these (corrupt_at, window) pairs \
20418 survived: {survivors:?}"
20419 );
20420 }
20421
20422 // ── Property coverage: the HPACK encoder across a multi-stream pass ──
20423 //
20424 // The deterministic test above fixes ONE shape: two streams, the HPACK
20425 // default table size, the size update landing on the first of exactly
20426 // two blocks. This property generalises the stream count (2..=5) and the
20427 // advertised table size, which decides both how many octets the §6.3
20428 // prefix occupies and how much dynamic table the later blocks can index.
20429 //
20430 // It is a sibling of `reassembly_property` below rather than a case
20431 // inside it: that one drives the READ path and its oracle is "one
20432 // decoder stays in sync across a fragmented header block", while this
20433 // one drives the WRITE path and its oracle is "one encoder, one prefix,
20434 // N blocks a single peer decoder replays". One `quickcheck` verdict over
20435 // two unrelated state machines would say nothing about either.
20436 //
20437 // The negative half of the deterministic test — a fresh decoder must
20438 // FAIL on a later block — is deliberately left out here: a generated
20439 // table size small enough to evict the dynamic table makes every block
20440 // self-contained, which is correct behaviour and would fail that
20441 // assertion.
20442 mod write_pass_property {
20443 use quickcheck::{Arbitrary, Gen, TestResult, quickcheck};
20444
20445 use super::*;
20446
20447 /// One generated write pass: how many streams share it, and the
20448 /// `SETTINGS_HEADER_TABLE_SIZE` the peer advertised just before it.
20449 #[derive(Debug, Clone, Copy)]
20450 struct WritePassPlan {
20451 streams: u8,
20452 table_size: u16,
20453 }
20454
20455 impl Arbitrary for WritePassPlan {
20456 fn arbitrary(g: &mut Gen) -> Self {
20457 WritePassPlan {
20458 streams: *g.choose(&[2u8, 3, 4, 5]).expect("non-empty slice"),
20459 // Bounded by the HPACK default a fresh `Decoder`
20460 // accepts, mirrored onto the peer decoder below; a
20461 // larger advertisement is a SETTINGS-path concern, not
20462 // a write-pass one.
20463 table_size: u16::arbitrary(g) % 4097,
20464 }
20465 }
20466 }
20467
20468 fn drive(plan: WritePassPlan) -> TestResult {
20469 let stream_ids: Vec<StreamId> =
20470 (0..u32::from(plan.streams)).map(|i| 1 + 2 * i).collect();
20471 let (blocks, residual) =
20472 drive_one_write_pass(&stream_ids, Some(u32::from(plan.table_size)));
20473
20474 if blocks
20475 .iter()
20476 .map(|(id, _)| *id)
20477 .ne(stream_ids.iter().copied())
20478 {
20479 return TestResult::error(format!(
20480 "plan {plan:?}: expected one block per stream in priority \
20481 order {stream_ids:?}, got {:?}",
20482 blocks.iter().map(|(id, _)| *id).collect::<Vec<_>>()
20483 ));
20484 }
20485 if !starts_with_size_update(&blocks[0].1) {
20486 return TestResult::error(format!(
20487 "plan {plan:?}: the first block must open with the \
20488 size update, got {:#04x}",
20489 blocks[0].1[0]
20490 ));
20491 }
20492 for (stream_id, block) in blocks.iter().skip(1) {
20493 if starts_with_size_update(block) {
20494 return TestResult::error(format!(
20495 "plan {plan:?}: stream {stream_id} re-emitted the \
20496 size update the first block already carried"
20497 ));
20498 }
20499 }
20500 if residual.is_some() {
20501 return TestResult::error(format!(
20502 "plan {plan:?}: the mirror must be cleared once a block \
20503 carried the signal, still {residual:?}"
20504 ));
20505 }
20506
20507 let mut peer_decoder = crate::protocol::mux::hpack::Decoder::new();
20508 peer_decoder.set_max_allowed_table_size(usize::from(plan.table_size));
20509 for (stream_id, block) in &blocks {
20510 let mut decoded = Vec::new();
20511 let status = peer_decoder.decode_with_cb(block, |key, value| {
20512 decoded.push((key.into_owned(), value.into_owned()));
20513 });
20514 if status.is_err() {
20515 return TestResult::error(format!(
20516 "plan {plan:?}: a peer replaying the pass in order \
20517 desynced on stream {stream_id}: {status:?}"
20518 ));
20519 }
20520 if !decoded.contains(&(b":status".to_vec(), b"404".to_vec())) {
20521 return TestResult::error(format!(
20522 "plan {plan:?}: stream {stream_id} decoded to \
20523 {decoded:?}, expected the 404 status line"
20524 ));
20525 }
20526 }
20527 TestResult::passed()
20528 }
20529
20530 quickcheck! {
20531 fn qc_one_write_pass_prefixes_its_first_header_block_only(plan: WritePassPlan) -> TestResult {
20532 drive(plan)
20533 }
20534 }
20535 }
20536
20537 // ── Property coverage: interleaved reassembly + control-frame flushes ──
20538 //
20539 // The four deterministic pinning tests above each fix ONE interleaving
20540 // (an unrelated WINDOW_UPDATE flush, a graceful_goaway, a HUP-while-
20541 // draining event between frames, a HUP-while-draining event mid-TCP-
20542 // segment) at ONE point. This property generalises them: a
20543 // `quickcheck`-driven `ReassemblyPlan` splits a real HPACK field block
20544 // into 2..=5 CONTINUATION fragments and independently chooses, before
20545 // each one, whether to interleave nothing, an unrelated WINDOW_UPDATE
20546 // flush, a `graceful_goaway`, or a HUP-while-draining event — the same
20547 // four triggers, now composed in every order and count `quickcheck`
20548 // cares to generate — and, independently again, whether that interleave
20549 // lands BETWEEN two frames or in the MIDDLE of one frame's own TCP-level
20550 // write (review of `e1c3c2fb`, B2/B3: the two shapes are not
20551 // interchangeable — `zero.storage` is empty in the former and holds a
20552 // genuine partial `socket_read()` in the latter, and only the latter
20553 // reaches the narrower guard the four deterministic tests individually
20554 // pin). `quickcheck` is already a workspace dependency (see
20555 // `lib/src/router/mod.rs`'s `qc_router_hostname_resolution_matches_
20556 // the_documented_semantics`, whose `Arbitrary`/`quickcheck!` shape this
20557 // mirrors).
20558 //
20559 // Any out-of-bounds read or other panic inside `drive()` fails the
20560 // `quickcheck` run directly — there is no separate assertion for it.
20561 // CONTINUATION-flood abort and the CVE-2024-27316 refusal path are
20562 // deliberately NOT folded into this property: they end the block in a
20563 // *different* terminal state (refused, not decoded), which would split
20564 // this property's single pass/fail oracle in two. That path already has
20565 // dedicated deterministic coverage —
20566 // `a_refused_stream_keeps_the_hpack_decoder_in_sync` and
20567 // `a_refused_padded_prioritized_stream_keeps_the_hpack_decoder_in_sync`
20568 // above.
20569 mod reassembly_property {
20570 use quickcheck::{Arbitrary, Gen, TestResult, quickcheck};
20571
20572 use super::*;
20573
20574 /// One interleaved event the property driver may inject before a
20575 /// CONTINUATION fragment. Mirrors the four deterministic pinning
20576 /// tests' triggers.
20577 #[derive(Debug, Clone, Copy)]
20578 enum Interleave {
20579 None,
20580 UnrelatedWindowUpdateFlush,
20581 HupWhileDraining,
20582 GracefulGoaway,
20583 }
20584
20585 impl Arbitrary for Interleave {
20586 fn arbitrary(g: &mut Gen) -> Self {
20587 *g.choose(&[
20588 Interleave::None,
20589 Interleave::UnrelatedWindowUpdateFlush,
20590 Interleave::HupWhileDraining,
20591 Interleave::GracefulGoaway,
20592 ])
20593 .expect("the choice slice above is non-empty")
20594 }
20595 }
20596
20597 /// One interleave point, paired with whether it lands between two
20598 /// frames (`mid_frame == false`, the original shape) or in the
20599 /// middle of the following fragment's own TCP-level write
20600 /// (`mid_frame == true` — a genuine partial `socket_read()`, the
20601 /// shape B2 proved is NOT interchangeable with the former).
20602 #[derive(Debug, Clone, Copy)]
20603 struct InterleavePoint {
20604 interleave: Interleave,
20605 mid_frame: bool,
20606 }
20607
20608 impl Arbitrary for InterleavePoint {
20609 fn arbitrary(g: &mut Gen) -> Self {
20610 InterleavePoint {
20611 interleave: Interleave::arbitrary(g),
20612 mid_frame: bool::arbitrary(g),
20613 }
20614 }
20615 }
20616
20617 /// How many CONTINUATION frames to split the field block into
20618 /// (2..=5) and which [`InterleavePoint`] to inject before each one
20619 /// after the first — exactly `splits - 1` points, one per fragment
20620 /// boundary, never `splits` (there is no point "before the first
20621 /// fragment": the HEADERS frame that carries it is what starts the
20622 /// reassembly).
20623 #[derive(Debug, Clone)]
20624 struct ReassemblyPlan {
20625 splits: u8,
20626 points: Vec<InterleavePoint>,
20627 }
20628
20629 impl Arbitrary for ReassemblyPlan {
20630 fn arbitrary(g: &mut Gen) -> Self {
20631 let splits = 2 + (u8::arbitrary(g) % 4); // 2..=5
20632 let points = (0..splits - 1)
20633 .map(|_| InterleavePoint::arbitrary(g))
20634 .collect();
20635 ReassemblyPlan { splits, points }
20636 }
20637 }
20638
20639 /// Apply one interleave. `GracefulGoaway` checks
20640 /// `connection.drain.draining()` — the AUTHORITATIVE state, not a
20641 /// separately tracked bool — before calling `graceful_goaway`:
20642 /// once the connection is already draining (whether a prior point
20643 /// in this same plan called `graceful_goaway` itself, OR a prior
20644 /// `HupWhileDraining` point set it via the test-only
20645 /// `__test_set_draining()` backdoor), a SECOND `graceful_goaway`
20646 /// call would be the RFC 9113 §6.8 FINAL GOAWAY
20647 /// (`GracefulDrainDecision::AlreadyDraining`), which drops
20648 /// `expect_read` and makes the block permanently uncompletable — a
20649 /// different scenario than this property drives, so a
20650 /// `GracefulGoaway` point reached while already draining is
20651 /// treated as `None` instead. A local `bool` tracking only
20652 /// "did GracefulGoaway itself fire" would miss the
20653 /// `HupWhileDraining` case and was exactly the bug `quickcheck`
20654 /// found while writing this property (review of `e1c3c2fb`, B3).
20655 fn apply_interleave<L>(
20656 connection: &mut H2Shell<mio::net::TcpStream>,
20657 context: &mut Context<L>,
20658 router: &mut Router,
20659 interleave: Interleave,
20660 ) where
20661 L: ListenerHandler + L7ListenerHandler,
20662 {
20663 match interleave {
20664 Interleave::None => {}
20665 Interleave::UnrelatedWindowUpdateFlush => {
20666 connection.core.queue_window_update(0, 65_535);
20667 connection.writable(context, EndpointClient(router));
20668 }
20669 Interleave::HupWhileDraining => {
20670 connection.core.drain.__test_set_draining();
20671 connection.core.readiness.event.insert(Ready::HUP);
20672 connection.writable(context, EndpointClient(router));
20673 connection.core.readiness.event.remove(Ready::HUP);
20674 }
20675 Interleave::GracefulGoaway => {
20676 if !connection.core.drain.draining() {
20677 let now = connection.core.now;
20678 connection.graceful_goaway(now);
20679 }
20680 }
20681 }
20682 }
20683
20684 /// Build the connection, split a real field block into
20685 /// `plan.splits` fragments, and send them as HEADERS + N-1
20686 /// CONTINUATION frames, injecting `plan.points[i - 1]` before
20687 /// fragment `i` (`i >= 1`) — either between the two frames, or
20688 /// split across two TCP-level writes of that fragment's OWN
20689 /// payload with the interleave firing in between. Asserts the
20690 /// connection stays usable and the block decodes byte-for-byte
20691 /// regardless of which interleaves fired or where.
20692 fn drive(plan: ReassemblyPlan) -> TestResult {
20693 use std::io::Write;
20694
20695 let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 20, 16_384)));
20696 let (mut connection, mut peer) = test_h2_connection(&pool, None);
20697 // The interleaved WINDOW_UPDATE flush really writes to the socket
20698 // since #1604 (it used to be deferred while a block was being
20699 // reassembled). Once sozu's end sends data, Linux switches it to
20700 // delayed ACKs ("pingpong" mode), so the peer's last segment is
20701 // acknowledged up to 40 ms late, and the peer's Nagle holds its
20702 // next small CONTINUATION until that ACK: longer than the
20703 // yield-bounded read loops below wait. H2 clients disable Nagle.
20704 peer.set_nodelay(true)
20705 .expect("the loopback peer must accept TCP_NODELAY");
20706 let mut context = test_context(&pool);
20707 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
20708
20709 connection.core.state = H2State::Header;
20710 connection
20711 .core
20712 .stream_table
20713 .set_expect_read(Some((H2StreamId::Zero, 9)));
20714
20715 let mut peer_encoder = crate::protocol::mux::hpack::Encoder::new();
20716 let field_block = peer_encoder.encode([
20717 (&b":method"[..], &b"GET"[..]),
20718 (&b":scheme"[..], &b"https"[..]),
20719 (&b":authority"[..], &b"example.com"[..]),
20720 (&b":path"[..], &b"/qc-multiframe"[..]),
20721 (&b"x-sozu-qc-marker"[..], &b"qc-value"[..]),
20722 ]);
20723 let splits = plan.splits as usize;
20724 if field_block.len() < splits * 2 {
20725 // Too few bytes to split `splits` ways meaningfully.
20726 return TestResult::discard();
20727 }
20728
20729 let chunk = (field_block.len() / splits).max(1);
20730 let mut fragments: Vec<&[u8]> = Vec::with_capacity(splits);
20731 let mut rest = &field_block[..];
20732 for i in 0..splits {
20733 if i + 1 == splits {
20734 fragments.push(rest);
20735 } else {
20736 let (a, b) = rest.split_at(chunk);
20737 fragments.push(a);
20738 rest = b;
20739 }
20740 }
20741 if fragments.iter().any(|f| f.is_empty()) {
20742 return TestResult::discard();
20743 }
20744
20745 let first = fragments[0];
20746 let mut frame = Vec::with_capacity(9 + first.len());
20747 frame.extend_from_slice(&(first.len() as u32).to_be_bytes()[1..]);
20748 frame.push(1); // HEADERS
20749 frame.push(parser::FLAG_END_STREAM);
20750 frame.extend_from_slice(&1u32.to_be_bytes());
20751 frame.extend_from_slice(first);
20752 if peer.write_all(&frame).is_err() || peer.flush().is_err() {
20753 return TestResult::discard();
20754 }
20755
20756 for _ in 0..64 {
20757 connection.readable(&mut context, EndpointClient(&mut router));
20758 if matches!(connection.core.state, H2State::ContinuationHeader(_)) {
20759 break;
20760 }
20761 std::thread::yield_now();
20762 }
20763 if !matches!(connection.core.state, H2State::ContinuationHeader(_)) {
20764 return TestResult::discard();
20765 }
20766
20767 for (idx, fragment) in fragments.iter().enumerate().skip(1) {
20768 let point = plan.points[idx - 1];
20769 let is_last = idx + 1 == fragments.len();
20770 let end_headers_flag = if is_last { parser::FLAG_END_HEADERS } else { 0 };
20771
20772 if point.mid_frame && fragment.len() >= 2 {
20773 // Split THIS fragment's own payload into two TCP-level
20774 // writes, firing the interleave in between — a genuine
20775 // partial `socket_read()`, not a gap between frames.
20776 let mid = fragment.len() / 2;
20777 let (segment_1, segment_2) = fragment.split_at(mid);
20778
20779 let mut header_and_segment_1 = Vec::with_capacity(9 + segment_1.len());
20780 header_and_segment_1
20781 .extend_from_slice(&(fragment.len() as u32).to_be_bytes()[1..]);
20782 header_and_segment_1.push(9); // CONTINUATION
20783 header_and_segment_1.push(end_headers_flag);
20784 header_and_segment_1.extend_from_slice(&1u32.to_be_bytes());
20785 header_and_segment_1.extend_from_slice(segment_1);
20786 if peer.write_all(&header_and_segment_1).is_err() || peer.flush().is_err() {
20787 return TestResult::discard();
20788 }
20789
20790 for _ in 0..64 {
20791 connection.readable(&mut context, EndpointClient(&mut router));
20792 if matches!(connection.core.state, H2State::ContinuationFrame(_))
20793 && matches!(
20794 connection.core.stream_table.expect_read(),
20795 Some((H2StreamId::Zero, remaining)) if remaining == segment_2.len()
20796 )
20797 {
20798 break;
20799 }
20800 std::thread::yield_now();
20801 }
20802 if !matches!(connection.core.state, H2State::ContinuationFrame(_)) {
20803 // The segment split did not land as a genuine
20804 // partial read (e.g. too small to observe) — treat
20805 // as a discard rather than a false failure.
20806 return TestResult::discard();
20807 }
20808
20809 apply_interleave(&mut connection, &mut context, &mut router, point.interleave);
20810
20811 if peer.write_all(segment_2).is_err() || peer.flush().is_err() {
20812 return TestResult::discard();
20813 }
20814 } else {
20815 apply_interleave(&mut connection, &mut context, &mut router, point.interleave);
20816
20817 let mut cframe = Vec::with_capacity(9 + fragment.len());
20818 cframe.extend_from_slice(&(fragment.len() as u32).to_be_bytes()[1..]);
20819 cframe.push(9); // CONTINUATION
20820 cframe.push(end_headers_flag);
20821 cframe.extend_from_slice(&1u32.to_be_bytes());
20822 cframe.extend_from_slice(fragment);
20823 if peer.write_all(&cframe).is_err() || peer.flush().is_err() {
20824 return TestResult::discard();
20825 }
20826 }
20827
20828 for _ in 0..64 {
20829 connection.readable(&mut context, EndpointClient(&mut router));
20830 if !matches!(
20831 connection.core.state,
20832 H2State::ContinuationHeader(_) | H2State::ContinuationFrame(_)
20833 ) {
20834 break;
20835 }
20836 std::thread::yield_now();
20837 }
20838 }
20839
20840 if matches!(connection.core.state, H2State::Error | H2State::GoAway) {
20841 return TestResult::error(format!(
20842 "connection reached {:?} while reassembling under plan {plan:?}",
20843 connection.core.state
20844 ));
20845 }
20846
20847 let next_block = peer_encoder.encode([(&b"x-sozu-qc-marker"[..], &b"qc-value"[..])]);
20848 let mut decoded = Vec::new();
20849 let status = connection
20850 .core
20851 .hpack
20852 .decoder_mut()
20853 .decode_with_cb(&next_block, |k, v| {
20854 decoded.push((k.into_owned(), v.into_owned()));
20855 });
20856 if status.is_err() {
20857 return TestResult::error(format!(
20858 "decoder desynced after plan {plan:?}: {status:?}"
20859 ));
20860 }
20861 if decoded != vec![(b"x-sozu-qc-marker".to_vec(), b"qc-value".to_vec())] {
20862 return TestResult::error(format!(
20863 "decoded {decoded:?} after plan {plan:?}, expected the qc marker header"
20864 ));
20865 }
20866
20867 TestResult::passed()
20868 }
20869
20870 quickcheck! {
20871 fn qc_h2_header_reassembly_survives_interleaved_control_frame_flushes(plan: ReassemblyPlan) -> TestResult {
20872 drive(plan)
20873 }
20874 }
20875 }
20876
20877 // ── #1454: the write path over a REAL `FrontRustls` ─────────────────
20878 //
20879 // Every `socket_wants_write()` branch exercised above this point is
20880 // exercised over `BackpressuredTlsSocket` — a TCP handler with a modelled
20881 // answer. #1454 rules that shape out for closing it: `FrontRustls` is the
20882 // only production handler that can answer `true`, and a fixture that fakes
20883 // the answer on a TCP handler tests the fake. These are the first tests in
20884 // the tree that build a `H2Shell<FrontRustls>`;
20885 // `git grep -cE 'H2Shell<FrontRustls>' -- '*.rs'` answered 0 on every
20886 // commit before this one, including on the `write_streams` inversion's head.
20887 //
20888 // What only the real handler can settle: `(size > 0, WouldBlock)` is not a
20889 // scripted pair here. It is what `FrontRustls::socket_write_vectored`
20890 // returns when rustls absorbs plaintext into its own record buffer while
20891 // `write_tls` blocks against a full kernel send queue — `buffered_size` and
20892 // `can_write` are two independent quantities sharing one return value.
20893 // `update_readiness` classifies a pass as stalled iff `size == 0`, so that
20894 // pair is NOT a stall and the flush loop must go round again. Everything
20895 // written against a scripted pair assumes the production handler produces
20896 // it; nothing proved it until these tests.
20897 //
20898 // The handshake runs in memory between a real `rustls::ServerConnection`
20899 // and a real `rustls::ClientConnection`, and only then is the settled
20900 // server session attached to a loopback socket whose kernel queues are
20901 // pinned small. That order matters: a flight never has to fit through the
20902 // shrunken queue, so the backpressure under test is entirely the test's
20903 // and never the handshake's.
20904
20905 use crate::socket::FrontRustls;
20906
20907 /// Accepts whatever certificate the frontend serves.
20908 ///
20909 /// `lib/assets/certificate.pem` is self-signed and carries NO
20910 /// subjectAltName, so rustls's own web-PKI verifier rejects it whatever
20911 /// root store it is given — a CN-only certificate is not verifiable under
20912 /// RFC 6125 §6.4.4 as rustls implements it. The client here is a
20913 /// decryption oracle for bytes the frontend wrote, not a party whose trust
20914 /// decision is under test, so it asserts validity outright. Same shape as
20915 /// `e2e`'s `Verifier`, which exists for the same certificate.
20916 #[derive(Debug)]
20917 struct AcceptAnyServerCertificate;
20918
20919 impl rustls::client::danger::ServerCertVerifier for AcceptAnyServerCertificate {
20920 fn verify_server_cert(
20921 &self,
20922 _end_entity: &rustls::pki_types::CertificateDer<'_>,
20923 _intermediates: &[rustls::pki_types::CertificateDer<'_>],
20924 _server_name: &rustls::pki_types::ServerName<'_>,
20925 _ocsp_response: &[u8],
20926 _now: rustls::pki_types::UnixTime,
20927 ) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
20928 Ok(rustls::client::danger::ServerCertVerified::assertion())
20929 }
20930
20931 fn verify_tls12_signature(
20932 &self,
20933 _message: &[u8],
20934 _cert: &rustls::pki_types::CertificateDer<'_>,
20935 _dss: &rustls::DigitallySignedStruct,
20936 ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
20937 Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
20938 }
20939
20940 fn verify_tls13_signature(
20941 &self,
20942 _message: &[u8],
20943 _cert: &rustls::pki_types::CertificateDer<'_>,
20944 _dss: &rustls::DigitallySignedStruct,
20945 ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
20946 Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
20947 }
20948
20949 fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
20950 vec![
20951 rustls::SignatureScheme::RSA_PKCS1_SHA256,
20952 rustls::SignatureScheme::RSA_PKCS1_SHA384,
20953 rustls::SignatureScheme::RSA_PKCS1_SHA512,
20954 rustls::SignatureScheme::ECDSA_NISTP256_SHA256,
20955 rustls::SignatureScheme::ECDSA_NISTP384_SHA384,
20956 rustls::SignatureScheme::ECDSA_NISTP521_SHA512,
20957 rustls::SignatureScheme::ED25519,
20958 rustls::SignatureScheme::RSA_PSS_SHA256,
20959 rustls::SignatureScheme::RSA_PSS_SHA384,
20960 rustls::SignatureScheme::RSA_PSS_SHA512,
20961 ]
20962 }
20963 }
20964
20965 /// SNI the test certificate is registered under.
20966 const TEST_SNI: &str = "lolcatho.st";
20967
20968 /// Kernel send/receive queue size for the loopback pair, in bytes. Linux
20969 /// doubles and floors the request, so the effective queue is a few KiB —
20970 /// an order of magnitude under rustls's own 64 KiB record buffer, which is
20971 /// what makes `socket_write_vectored` return a partial count AND
20972 /// `WouldBlock` together instead of one or the other.
20973 const PINNED_SOCKET_QUEUE: usize = 4096;
20974
20975 /// Bytes the tests below queue as a response. Larger than rustls's record
20976 /// buffer, so one `socket_write_vectored` cannot absorb it all.
20977 const QUEUED_RESPONSE_LEN: usize = 256 * 1024;
20978
20979 /// Blocks the response is split into, so a gather offers several slices.
20980 const QUEUED_RESPONSE_CHUNK_LEN: usize = QUEUED_RESPONSE_LEN / 8;
20981
20982 /// Hard bound on the drive loops below. Reaching it is a test failure with
20983 /// the byte shortfall attached, never a silent pass.
20984 const MAX_DRIVE_TICKS: usize = 4096;
20985
20986 /// The response bytes, leaked once so blocks can be `Store::Static`.
20987 ///
20988 /// A repeating non-trivial pattern rather than a constant fill: a
20989 /// comparison against a constant fill cannot see bytes delivered out of
20990 /// order or a block delivered twice, which is half of what "no truncation"
20991 /// has to mean.
20992 ///
20993 /// Laid out as whole DATA frames on stream 1, each 16 KiB on the wire: a
20994 /// stream's `kawa.out` only ever holds whole H2 frames, and since #1604
20995 /// the write pass reads their headers to find where the frame a partial
20996 /// write cut ends (`h2_transmit::frame_tail`). The pattern runs through
20997 /// the headers as well, so the oracle still compares every byte.
20998 fn queued_response_body() -> &'static [u8] {
20999 const FRAME_LEN: usize = 16 * 1024;
21000 const PAYLOAD_LEN: usize = FRAME_LEN - 9;
21001 const _: () = assert!(QUEUED_RESPONSE_LEN.is_multiple_of(FRAME_LEN));
21002 static BODY: std::sync::OnceLock<&'static [u8]> = std::sync::OnceLock::new();
21003 BODY.get_or_init(|| {
21004 let mut body: Vec<u8> = (0..QUEUED_RESPONSE_LEN)
21005 .map(|index| (index % 251) as u8)
21006 .collect();
21007 for frame in body.chunks_mut(FRAME_LEN) {
21008 frame[..9].copy_from_slice(&[
21009 (PAYLOAD_LEN >> 16) as u8,
21010 (PAYLOAD_LEN >> 8) as u8,
21011 PAYLOAD_LEN as u8,
21012 0,
21013 0,
21014 0,
21015 0,
21016 0,
21017 1,
21018 ]);
21019 }
21020 Box::leak(body.into_boxed_slice())
21021 })
21022 }
21023
21024 /// Move one TLS flight from `from` to `to`, in memory.
21025 fn pump_tls<A, B>(
21026 from: &mut rustls::ConnectionCommon<A>,
21027 to: &mut rustls::ConnectionCommon<B>,
21028 ) {
21029 let mut wire = Vec::new();
21030 while from.wants_write() {
21031 from.write_tls(&mut wire)
21032 .expect("a TLS flight must serialize into memory");
21033 }
21034 if wire.is_empty() {
21035 return;
21036 }
21037 let mut cursor = std::io::Cursor::new(wire.as_slice());
21038 while (cursor.position() as usize) < wire.len() {
21039 to.read_tls(&mut cursor)
21040 .expect("a TLS flight must be readable from memory");
21041 to.process_new_packets()
21042 .expect("a TLS flight must process cleanly");
21043 }
21044 }
21045
21046 /// A settled TLS 1.3 `FrontRustls` over a loopback socket whose kernel
21047 /// queues are pinned small, the peer end of that socket, and the client
21048 /// session that decrypts what the frontend writes.
21049 ///
21050 /// The peer and the client session are returned rather than kept here
21051 /// because dropping either would close the connection under the handler
21052 /// being tested, and because together they are the only oracle for "the
21053 /// bytes actually arrived".
21054 fn handshaken_front_rustls() -> (FrontRustls, std::net::TcpStream, rustls::ClientConnection) {
21055 let provider = std::sync::Arc::new(crate::crypto::default_provider());
21056
21057 let resolver = std::sync::Arc::new(crate::tls::MutexCertificateResolver::default());
21058 resolver
21059 .0
21060 .lock()
21061 .expect("the test resolver lock must be available")
21062 .add_certificate(&sozu_command::proto::command::AddCertificate {
21063 address: sozu_command::proto::command::SocketAddress::new_v4(127, 0, 0, 1, 8443),
21064 certificate: sozu_command::proto::command::CertificateAndKey {
21065 certificate: include_str!("../../../assets/certificate.pem").to_owned(),
21066 key: include_str!("../../../assets/key.pem").to_owned(),
21067 names: vec![TEST_SNI.to_owned()],
21068 ..Default::default()
21069 },
21070 expired_at: None,
21071 })
21072 .expect("the test certificate must load into the resolver");
21073
21074 let mut server_config = rustls::ServerConfig::builder_with_provider(provider.clone())
21075 .with_protocol_versions(&[&rustls::version::TLS13])
21076 .expect("the test provider must support TLS 1.3")
21077 .with_no_client_auth()
21078 .with_cert_resolver(resolver);
21079 // TLS 1.3 queues NewSessionTicket straight after the client Finished.
21080 // A server that sends tickets answers `socket_wants_write()` with
21081 // `true` before this fixture has queued a single response byte, and
21082 // every test below would then read `true` for the wrong reason.
21083 server_config.send_tls13_tickets = 0;
21084
21085 let client_config = rustls::ClientConfig::builder_with_provider(provider)
21086 .with_protocol_versions(&[&rustls::version::TLS13])
21087 .expect("the test provider must support TLS 1.3")
21088 .dangerous()
21089 .with_custom_certificate_verifier(std::sync::Arc::new(AcceptAnyServerCertificate))
21090 .with_no_client_auth();
21091
21092 let mut server = rustls::ServerConnection::new(std::sync::Arc::new(server_config))
21093 .expect("the test server session must initialize");
21094 let mut client = rustls::ClientConnection::new(
21095 std::sync::Arc::new(client_config),
21096 rustls::pki_types::ServerName::try_from(TEST_SNI)
21097 .expect("the test SNI must be a valid DNS name"),
21098 )
21099 .expect("the test client session must initialize");
21100
21101 for _ in 0..MAX_DRIVE_TICKS {
21102 if !client.is_handshaking()
21103 && !server.is_handshaking()
21104 && !client.wants_write()
21105 && !server.wants_write()
21106 {
21107 break;
21108 }
21109 pump_tls(&mut client, &mut server);
21110 pump_tls(&mut server, &mut client);
21111 }
21112
21113 assert!(
21114 !server.is_handshaking() && !client.is_handshaking(),
21115 "premise: the in-memory handshake must complete, or the session \
21116 under test buffers plaintext instead of encrypting it"
21117 );
21118 assert!(
21119 !server.wants_write(),
21120 "premise: a settled handshake must leave the server holding no \
21121 records, or socket_wants_write() answers true before this fixture \
21122 has queued one response byte"
21123 );
21124
21125 let listener =
21126 std::net::TcpListener::bind("127.0.0.1:0").expect("the test listener must bind");
21127 // Set on the listener so the accepted socket inherits it and the
21128 // advertised window starts small.
21129 socket2::SockRef::from(&listener)
21130 .set_recv_buffer_size(PINNED_SOCKET_QUEUE)
21131 .expect("the test listener receive queue must shrink");
21132 let address = listener
21133 .local_addr()
21134 .expect("the test listener must expose its address");
21135 let front = std::net::TcpStream::connect(address).expect("the loopback connect must land");
21136 let (peer, _peer_address) = listener.accept().expect("the test peer must be accepted");
21137 socket2::SockRef::from(&front)
21138 .set_send_buffer_size(PINNED_SOCKET_QUEUE)
21139 .expect("the frontend send queue must shrink");
21140 socket2::SockRef::from(&peer)
21141 .set_recv_buffer_size(PINNED_SOCKET_QUEUE)
21142 .expect("the peer receive queue must shrink");
21143 front
21144 .set_nonblocking(true)
21145 .expect("mio requires a non-blocking stream");
21146 peer.set_nonblocking(true)
21147 .expect("the peer must not block the drive loop");
21148
21149 let socket = FrontRustls {
21150 stream: mio::net::TcpStream::from_std(front),
21151 session: server,
21152 peer_disconnected: false,
21153 peer_reset: false,
21154 tls_fatal: false,
21155 session_ulid: Ulid::generate(),
21156 configured_peer: None,
21157 recv_memory: Default::default(),
21158 };
21159 (socket, peer, client)
21160 }
21161
21162 /// A server `ConnectionH2` over that handler.
21163 ///
21164 /// `state` is a parameter for the same reason
21165 /// `connection_with_backpressure` takes one: the close decision reads it
21166 /// as `H2State::GoAway` and the write pass reads it as `H2State::Header`.
21167 pub(in crate::protocol::mux) fn rustls_h2_connection(
21168 pool: &Rc<RefCell<Pool>>,
21169 state: H2State,
21170 ) -> (
21171 H2Shell<FrontRustls>,
21172 std::net::TcpStream,
21173 rustls::ClientConnection,
21174 ) {
21175 let (socket, peer, client) = handshaken_front_rustls();
21176 let mut connection = H2Shell::new(
21177 Ulid::generate(),
21178 socket,
21179 Position::Server,
21180 &mut PoolBufferSource::new(Rc::downgrade(pool)),
21181 H2FloodConfig::default(),
21182 H2ConnectionConfig::default(),
21183 Duration::from_secs(30),
21184 None,
21185 Duration::from_secs(30),
21186 Some((H2StreamId::Zero, CLIENT_PREFACE_SIZE)),
21187 Ready::WRITABLE | Ready::HUP | Ready::ERROR,
21188 )
21189 .expect("a pool with free buffers must yield an H2 connection");
21190 connection.core.state = state;
21191 (connection, peer, client)
21192 }
21193
21194 /// Read every TLS record the peer's kernel currently holds, decrypt it,
21195 /// and append the plaintext to `received`.
21196 fn drain_peer(
21197 client: &mut rustls::ClientConnection,
21198 peer: &mut std::net::TcpStream,
21199 received: &mut Vec<u8>,
21200 ) {
21201 loop {
21202 match client.read_tls(peer) {
21203 Ok(0) => break,
21204 Ok(_) => {}
21205 Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => break,
21206 Err(error) => {
21207 panic!("the peer must read the frontend's TLS records: {error:?}")
21208 }
21209 }
21210 client
21211 .process_new_packets()
21212 .expect("the peer must decrypt what the frontend wrote");
21213 take_plaintext(client, received);
21214 }
21215 take_plaintext(client, received);
21216 }
21217
21218 /// Drain the client session's decrypted plaintext buffer.
21219 ///
21220 /// `WouldBlock` here means "nothing decrypted yet", not an error, and
21221 /// `read_to_end` appends whatever it did read before reporting it.
21222 fn take_plaintext(client: &mut rustls::ClientConnection, received: &mut Vec<u8>) {
21223 use std::io::Read as _;
21224 let mut plaintext = Vec::new();
21225 match client.reader().read_to_end(&mut plaintext) {
21226 Ok(_) => {}
21227 Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {}
21228 Err(error) => panic!("the peer must decrypt the frontend's records: {error:?}"),
21229 }
21230 received.extend_from_slice(&plaintext);
21231 }
21232
21233 /// Keep flushing the handler's buffered records while the peer reads,
21234 /// until rustls holds nothing. Returns whether it got there.
21235 fn flush_until_drained(
21236 socket: &mut FrontRustls,
21237 client: &mut rustls::ClientConnection,
21238 peer: &mut std::net::TcpStream,
21239 received: &mut Vec<u8>,
21240 ) -> bool {
21241 for _ in 0..MAX_DRIVE_TICKS {
21242 if !socket.socket_wants_write() {
21243 return true;
21244 }
21245 // Loopback delivery is softirq-driven, not synchronous with
21246 // `write_tls`: give it a slot rather than spinning the CPU.
21247 std::thread::yield_now();
21248 drain_peer(client, peer, received);
21249 socket.socket_write(&[]);
21250 }
21251 !socket.socket_wants_write()
21252 }
21253
21254 /// A TLS frontend whose final GOAWAY went out without `close_notify`
21255 /// behind it: the close its next writable pass decides queues the alert
21256 /// first and waits a pass for it, then starts the lingering close. The
21257 /// first pass's close moves the state to `Error`; the pending linger is
21258 /// what keeps the second pass from closing outright. The client reads
21259 /// the alert, then the EOF of the half-close.
21260 ///
21261 /// TO SEE THIS RED: in `H2Shell::linger_instead_of_closing`, drop the
21262 /// `self.core.linger = Linger::Pending { deadline };` before the deferral.
21263 /// The second pass then returns `CloseSession`.
21264 #[test]
21265 fn a_linger_deferred_behind_close_notify_starts_on_the_next_pass() {
21266 use std::io::Read as _;
21267
21268 let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
21269 let (mut connection, mut peer, mut client) = rustls_h2_connection(&pool, H2State::GoAway);
21270 connection.core.drain.__test_set_draining();
21271 connection.core.graceful_goaway = true;
21272 let mut context = test_context(&pool);
21273 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
21274
21275 let first = connection.writable(&mut context, EndpointClient(&mut router));
21276 assert!(
21277 matches!(first, MuxResult::Continue) && !connection.core.is_lingering(),
21278 "the first pass waits for close_notify, got {first:?}"
21279 );
21280 let mut second = MuxResult::Continue;
21281 for _ in 0..MAX_DRIVE_TICKS {
21282 connection.core.readiness.event.insert(Ready::WRITABLE);
21283 second = connection.writable(&mut context, EndpointClient(&mut router));
21284 if connection.core.is_lingering() || !matches!(second, MuxResult::Continue) {
21285 break;
21286 }
21287 std::thread::yield_now();
21288 }
21289 assert!(
21290 matches!(second, MuxResult::Continue) && connection.core.is_lingering(),
21291 "once close_notify is out the connection lingers, got {second:?}"
21292 );
21293
21294 let mut received = Vec::new();
21295 let mut closed = false;
21296 for _ in 0..MAX_DRIVE_TICKS {
21297 drain_peer(&mut client, &mut peer, &mut received);
21298 let mut plaintext = [0u8; 16];
21299 match client.reader().read(&mut plaintext) {
21300 Ok(0) => {
21301 closed = true;
21302 break;
21303 }
21304 Ok(_) => {}
21305 Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
21306 std::thread::yield_now()
21307 }
21308 Err(error) => panic!("the client must see a clean close: {error:?}"),
21309 }
21310 }
21311 assert!(
21312 closed,
21313 "the client reads close_notify, then the end of the stream"
21314 );
21315 }
21316
21317 /// The final GOAWAY and the TLS `close_notify` leave in ONE write, and the
21318 /// peer reads the GOAWAY before the close.
21319 ///
21320 /// Drives what a client GOAWAY on an idle connection sets off:
21321 /// `goaway(NoError)`, the `writable` pass that flushes it and starts the
21322 /// lingering close, and `close`. Every TLS write in between is counted through
21323 /// `crate::socket::tls_writes`, which counts each `write_tls` holding at
21324 /// least one record, i.e. each `writev(2)` `FrontRustls` issues. The wire
21325 /// is then read raw, so the ORDER is checked on the records themselves
21326 /// (TLS 1.3 hides the inner type, not the length: 5 + 17 + 1 + 16 = 39 for
21327 /// the GOAWAY, 5 + 2 + 1 + 16 = 24 for the alert), and decrypted, so the
21328 /// client sees the 17-byte GOAWAY and then a clean end of stream, which
21329 /// rustls reports only once it has read `close_notify`.
21330 ///
21331 /// TO SEE THIS RED: in `H2Shell::flush_output_to_socket`, drop the
21332 /// `output_flush_closes_connection` branch so every output flush goes
21333 /// through `socket_write`. The GOAWAY then leaves alone and `close` sends the
21334 /// alert in a second write: this test fails on `the final GOAWAY and
21335 /// close_notify must share one TLS write`, with 2 writes. For the order:
21336 /// in `FrontRustls::socket_write_then_close`, call `send_close_notify`
21337 /// before `writer().write`; it fails on the records, `[24, 39]`.
21338 #[test]
21339 fn the_final_goaway_and_close_notify_leave_in_one_tls_write() {
21340 use std::io::Read as _;
21341
21342 let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
21343 let (mut connection, mut peer, mut client) = rustls_h2_connection(&pool, H2State::Header);
21344 let mut context = test_context(&pool);
21345 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
21346
21347 let writes_before = crate::socket::tls_writes();
21348 let queued = connection.core.goaway(H2Error::NoError);
21349 assert!(
21350 matches!(queued, MuxResult::Continue),
21351 "premise: the GOAWAY must serialise, got {queued:?}"
21352 );
21353 let result = connection.writable(&mut context, EndpointClient(&mut router));
21354 // The close that pass decides is a lingering one: the write side is
21355 // shut down behind the GOAWAY and `close_notify`, and what the client
21356 // still sends is drained (`H2Shell::linger_instead_of_closing`).
21357 assert!(
21358 matches!(result, MuxResult::Continue) && connection.core.is_lingering(),
21359 "the pass that flushed the final GOAWAY must start the lingering close, got {result:?}"
21360 );
21361 connection.close(&mut context, EndpointClient(&mut router));
21362 assert_eq!(
21363 crate::socket::tls_writes() - writes_before,
21364 1,
21365 "the final GOAWAY and close_notify must share one TLS write"
21366 );
21367
21368 let mut wire = Vec::new();
21369 for _ in 0..MAX_DRIVE_TICKS {
21370 let mut chunk = [0u8; 256];
21371 match peer.read(&mut chunk) {
21372 Ok(0) => break,
21373 Ok(size) => wire.extend_from_slice(&chunk[..size]),
21374 Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
21375 if wire.len() >= 39 + 24 {
21376 break;
21377 }
21378 std::thread::yield_now();
21379 }
21380 Err(error) => panic!("the peer must read the frontend's records: {error:?}"),
21381 }
21382 }
21383 let mut records = Vec::new();
21384 let mut offset = 0;
21385 while offset + 5 <= wire.len() {
21386 let length = 5 + usize::from(u16::from_be_bytes([wire[offset + 3], wire[offset + 4]]));
21387 records.push(length);
21388 offset += length;
21389 }
21390 assert_eq!(
21391 records,
21392 vec![39, 24],
21393 "the peer must receive the GOAWAY record, then the close_notify record"
21394 );
21395
21396 let mut cursor = std::io::Cursor::new(wire.as_slice());
21397 while (cursor.position() as usize) < wire.len() {
21398 client
21399 .read_tls(&mut cursor)
21400 .expect("the peer must read the frontend's records");
21401 client
21402 .process_new_packets()
21403 .expect("the peer must decrypt the frontend's records");
21404 }
21405 let mut frame = [0u8; 64];
21406 let size = client
21407 .reader()
21408 .read(&mut frame)
21409 .expect("the GOAWAY must be readable before the close");
21410 assert_eq!(size, 17, "the GOAWAY frame is 9 + 8 bytes");
21411 assert_eq!(frame[3], 0x07, "the first plaintext must be a GOAWAY frame");
21412 assert_eq!(
21413 &frame[13..17],
21414 &[0, 0, 0, 0],
21415 "the GOAWAY must carry NO_ERROR"
21416 );
21417 assert_eq!(
21418 client
21419 .reader()
21420 .read(&mut frame)
21421 .expect("a close_notify ends the stream cleanly"),
21422 0,
21423 "the close_notify must follow the GOAWAY"
21424 );
21425 }
21426
21427 /// `FrontRustls` answers a blocked write with bytes taken AND `WouldBlock`,
21428 /// and `update_readiness` calls that pair not-stalled.
21429 ///
21430 /// This is the premise every scripted `(size > 0, WouldBlock)` test in this
21431 /// file rests on, taken here from the production handler instead of from a
21432 /// fixture: rustls absorbs plaintext into its record buffer while
21433 /// `write_tls` blocks against the kernel, so `buffered_size` and
21434 /// `can_write` disagree and one return value carries both answers.
21435 ///
21436 /// TO SEE THIS RED: in `FrontRustls::socket_write_vectored`, report
21437 /// nothing absorbed on a blocked write — change the `!can_write` arm of
21438 /// the final result from `(buffered_size, SocketResult::WouldBlock)` to
21439 /// `(0, SocketResult::WouldBlock)`. That is the under-reporting half of
21440 /// the same class the over-reporting one below belongs to, and the caller
21441 /// then re-offers bytes rustls already holds. This test fails on `rustls
21442 /// takes plaintext off the caller's hands even while the kernel is full,
21443 /// so the handler must report the bytes it absorbed`. Measured: `1092
21444 /// passed; 6 failed` — every test in this block and nothing else, so the
21445 /// whole class was invisible to the suite.
21446 ///
21447 /// Not a red recipe, and measured as such: deleting `can_write = false;`
21448 /// from the `ErrorKind::WouldBlock` arm of the partial-absorb draining
21449 /// loop leaves the entire suite green. The post-loop flush block sets the
21450 /// same flag for the same reason a moment later, so the `WouldBlock` half
21451 /// of this pair is guarded twice in production and no single-site deletion
21452 /// of it is observable here.
21453 #[test]
21454 fn a_real_rustls_frontend_reports_bytes_taken_and_would_block_together() {
21455 let (mut socket, _peer, _client) = handshaken_front_rustls();
21456 let body = queued_response_body();
21457
21458 assert!(
21459 !socket.socket_wants_write(),
21460 "premise: the fixture must start with rustls holding nothing"
21461 );
21462
21463 let (size, status) = socket.socket_write_vectored(&[IoSlice::new(body)]);
21464
21465 assert!(
21466 size > 0,
21467 "rustls takes plaintext off the caller's hands even while the \
21468 kernel is full, so the handler must report the bytes it absorbed"
21469 );
21470 assert!(
21471 size < body.len(),
21472 "premise: the offer must exceed what one call can absorb, or the \
21473 partial-write path that produces this pair is never entered \
21474 (absorbed {size} of {})",
21475 body.len()
21476 );
21477 assert_eq!(
21478 status,
21479 SocketResult::WouldBlock,
21480 "a frontend whose kernel send queue is full must report WouldBlock"
21481 );
21482 assert!(
21483 socket.socket_wants_write(),
21484 "the records rustls could not push must still be waiting, which is \
21485 the only reason any close path has to ask"
21486 );
21487
21488 let mut readiness = Readiness {
21489 event: Ready::WRITABLE,
21490 interest: Ready::WRITABLE | Ready::HUP | Ready::ERROR,
21491 };
21492 assert!(
21493 !update_readiness_after_write(size, status, &mut readiness),
21494 "a write that moved bytes is not a stall however it reported its \
21495 status: a pass that ends here leaves the rest of the response \
21496 queued behind a WouldBlock the rule does not treat as one"
21497 );
21498 }
21499
21500 /// The empty flush the close paths issue really does hand rustls's records
21501 /// to the kernel — `true` before it and `false` after, once the peer reads.
21502 ///
21503 /// `socket_write(&[])` is the middle call of every flush triple in
21504 /// `h2.rs`: those sites discard its `(size, status)` entirely and learn
21505 /// whether it landed only by asking `socket_wants_write()` again. That
21506 /// reconstruction is only sound if a zero-length write flushes at all, and
21507 /// `FrontRustls::socket_write`'s main loop exits immediately for an empty
21508 /// buffer — the flush lives in a block after it, reached by no other path.
21509 ///
21510 /// TO SEE THIS RED: in `FrontRustls::socket_write`, delete the post-loop
21511 /// `if !is_error && !is_closed && can_write && self.session.wants_write()`
21512 /// block. `socket_write(&[])` becomes a no-op, the records never reach the
21513 /// kernel, and this test fails on `once the peer reads, the empty flush
21514 /// must hand every record to the kernel: rustls still holds records after
21515 /// 4096 flushes against a draining peer`. Measured: `1093 passed; 5
21516 /// failed` — five of the six here and nothing that predates them, so the
21517 /// block that comment calls load-bearing had no unit coverage at all.
21518 #[test]
21519 fn an_empty_flush_drains_the_records_a_blocked_write_left_behind() {
21520 let (mut socket, mut peer, mut client) = handshaken_front_rustls();
21521 let body = queued_response_body();
21522
21523 let (absorbed, _status) = socket.socket_write_vectored(&[IoSlice::new(body)]);
21524 assert!(
21525 socket.socket_wants_write(),
21526 "premise: a blocked kernel must leave records pending, or neither \
21527 arm below is reachable"
21528 );
21529
21530 // The kernel is still full: the flush cannot land and the query
21531 // answers `true` on both sides of it. This is the "true twice" arm.
21532 socket.socket_write(&[]);
21533 assert!(
21534 socket.socket_wants_write(),
21535 "a flush against a full kernel leaves the records where they were, \
21536 so the triple's second query must still answer true"
21537 );
21538
21539 // Now the peer reads, and the same flush must land. This is the
21540 // "true then false" arm.
21541 let mut received = Vec::new();
21542 assert!(
21543 flush_until_drained(&mut socket, &mut client, &mut peer, &mut received),
21544 "once the peer reads, the empty flush must hand every record to \
21545 the kernel: rustls still holds records after {MAX_DRIVE_TICKS} \
21546 flushes against a draining peer"
21547 );
21548
21549 for _ in 0..MAX_DRIVE_TICKS {
21550 if received.len() >= absorbed {
21551 break;
21552 }
21553 std::thread::yield_now();
21554 drain_peer(&mut client, &mut peer, &mut received);
21555 }
21556 assert_eq!(
21557 received.len(),
21558 absorbed,
21559 "every plaintext byte rustls reported absorbing must reach the peer"
21560 );
21561 assert_eq!(
21562 received.as_slice(),
21563 &body[..absorbed],
21564 "the peer must receive those bytes unchanged and in order"
21565 );
21566 }
21567
21568 /// A response larger than one pass can deliver reaches the peer in full,
21569 /// driven through `writable()` over the production TLS handler.
21570 ///
21571 /// This is #1454's truncation vector end to end, with no scripted answer
21572 /// anywhere: the `(size > 0, WouldBlock)` and `(0, WouldBlock)` pairs come
21573 /// from `FrontRustls` against a real full kernel, the stall and its resume
21574 /// come from `write_streams`, and the oracle is the bytes a real
21575 /// `rustls::ClientConnection` decrypts on the other end of the socket. The
21576 /// assertion is byte equality, not a call count, so it says nothing about
21577 /// how the write machine is shaped and everything about what it delivers.
21578 ///
21579 /// TO SEE THIS RED: in `FrontRustls::socket_write_vectored`, report the
21580 /// full offer instead of what rustls took — replace the final
21581 /// `(buffered_size, SocketResult::WouldBlock)` with
21582 /// `(total_len, SocketResult::WouldBlock)`. That is the over-reporting
21583 /// shape `socket_write`/`socket_write_vectored`'s own comments call the
21584 /// 4.5 MB truncation-class bug: `h2_transmit::confirm` then consumes bytes
21585 /// the socket never encrypted and they are gone. Measured: `1095 passed; 3
21586 /// failed`, this one on `the response was truncated at 65536 of 262144
21587 /// bytes after 4096 passes, with 0 blocks still queued`. The pass empties
21588 /// `kawa.out` against bytes that were never sent, `finalize_write` reads
21589 /// that as everything written and quiesces, and the remaining 196608 bytes
21590 /// have no route to the wire at all — the connection is not short, it is
21591 /// hung, and the tick cap is what converts that into a failure instead of
21592 /// a wait. Nothing that predates this block moves.
21593 #[test]
21594 fn a_blocked_rustls_frontend_delivers_every_queued_byte_across_passes() {
21595 let pool = make_pool_for_invariant_16();
21596 let (mut connection, mut peer, mut client) = rustls_h2_connection(&pool, H2State::Header);
21597 let mut context = test_context(&pool);
21598 let body = queued_response_body();
21599
21600 let gid = context
21601 .create_stream(Ulid::generate(), 1 << 16)
21602 .expect("test context must create a stream");
21603 connection
21604 .core
21605 .stream_table
21606 .register(REGISTERED_STREAM_ID, gid, connection.core.now);
21607 for chunk in body.chunks(QUEUED_RESPONSE_CHUNK_LEN) {
21608 context.streams[gid]
21609 .back
21610 .out
21611 .push_back(kawa::OutBlock::Store(kawa::Store::Static(chunk)));
21612 }
21613 assert_prepare_gate_is_shut(&context, gid);
21614 assert_eq!(
21615 connection.core.stream_table.expect_write(),
21616 None,
21617 "premise: no parked write, so the first pass takes the main loop"
21618 );
21619
21620 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
21621 let mut received = Vec::new();
21622 let mut ticks = 0usize;
21623
21624 loop {
21625 // What the event loop does: report the socket writable, run the
21626 // pass, and let the peer read whatever reached the kernel.
21627 connection.core.readiness.event.insert(Ready::WRITABLE);
21628 connection.writable(&mut context, EndpointClient(&mut router));
21629 std::thread::yield_now();
21630 drain_peer(&mut client, &mut peer, &mut received);
21631
21632 if received.len() >= body.len() {
21633 break;
21634 }
21635 ticks += 1;
21636 assert!(
21637 ticks < MAX_DRIVE_TICKS,
21638 "the response was truncated at {} of {} bytes after {ticks} \
21639 passes, with {} blocks still queued",
21640 received.len(),
21641 body.len(),
21642 context.streams[gid].back.out.len()
21643 );
21644 // Owed, not undelivered: once `kawa.out` is empty and rustls
21645 // holds nothing, every byte has been handed to the kernel and
21646 // `finalize_write` may legitimately quiesce while the last of it
21647 // is still crossing loopback. Asserting on `received` here would
21648 // blame the write machine for the harness's own race.
21649 let still_owed =
21650 !context.streams[gid].back.out.is_empty() || connection.socket.socket_wants_write();
21651 assert!(
21652 !still_owed || connection.core.readiness.interest.is_writable(),
21653 "a pass that still owes bytes must keep WRITABLE interest, or \
21654 no later event reaches this connection and the response is \
21655 truncated at {} of {} bytes with {} blocks queued",
21656 received.len(),
21657 body.len(),
21658 context.streams[gid].back.out.len()
21659 );
21660 }
21661
21662 assert_eq!(
21663 received.len(),
21664 body.len(),
21665 "the peer must receive every queued byte"
21666 );
21667 assert_eq!(
21668 received.as_slice(),
21669 body,
21670 "the peer must receive the queued bytes unchanged and in order"
21671 );
21672 assert!(
21673 context.streams[gid].back.out.is_empty(),
21674 "every queued block must be consumed once the response is delivered"
21675 );
21676 assert_eq!(
21677 context.streams[gid].metrics.bout,
21678 body.len(),
21679 "the stream must account exactly the bytes the socket took"
21680 );
21681 }
21682
21683 /// `force_disconnect`'s server arm, over the handler whose
21684 /// `socket_wants_write()` is the only production implementation that can
21685 /// answer `true`: it re-arms while rustls holds records and closes once
21686 /// they are gone.
21687 ///
21688 /// One test for both arms because the TRANSITION is the claim — the same
21689 /// connection, the same records, and the only thing that changes is
21690 /// whether the peer read them. A pair of connections would prove the two
21691 /// answers of a table `h2_close::force_disconnect_action` already tables
21692 /// exhaustively; what has never been proved is that this caller reads a
21693 /// real handler's answer at all.
21694 ///
21695 /// TO SEE THIS RED: in `ConnectionH2::force_disconnect`, replace
21696 /// `let tls_wants_write = self.tls_wants_write();` with
21697 /// `let tls_wants_write = false;` — the decision stops reading the socket,
21698 /// and the call closes the session while records are pending, which sends
21699 /// FIN and destroys them. This test fails on `a disconnect with records
21700 /// pending must not close the session: closing sends FIN and destroys the
21701 /// records rustls still holds, which the peer reads as a truncated
21702 /// response`. Measured: `1096 passed; 2 failed` — this test and
21703 /// `force_disconnect_re_arms_while_tls_records_are_pending`, the same
21704 /// decision with one witness over a modelled handler and one over the
21705 /// production one.
21706 #[test]
21707 fn force_disconnect_over_a_real_rustls_frontend_waits_for_the_records_to_drain() {
21708 let pool = make_pool_for_invariant_16();
21709 let (mut connection, mut peer, mut client) = rustls_h2_connection(&pool, H2State::GoAway);
21710 let body = queued_response_body();
21711
21712 let (absorbed, _status) = connection
21713 .socket
21714 .socket_write_vectored(&[IoSlice::new(body)]);
21715 assert!(
21716 absorbed > 0 && connection.socket.socket_wants_write(),
21717 "premise: the blocked kernel must leave real records pending"
21718 );
21719 assert!(
21720 !connection.core.peer_gone_after_final_goaway(),
21721 "premise: the peer must still be live, or the decision \
21722 short-circuits on its first boolean and never reads the socket"
21723 );
21724
21725 assert!(
21726 matches!(connection.force_disconnect(), MuxResult::Continue),
21727 "a disconnect with records pending must not close the session: \
21728 closing sends FIN and destroys the records rustls still holds, \
21729 which the peer reads as a truncated response"
21730 );
21731 assert!(
21732 connection.core.readiness.interest.is_writable(),
21733 "the delayed close must keep WRITABLE interest so the write path \
21734 can still flush"
21735 );
21736 // The records were written straight to the socket, outside any
21737 // writable pass, so no refusal is recorded and the delayed close
21738 // re-raises WRITABLE for the write path to run once more. A write the
21739 // same pass saw refused would leave it to the kernel's edge instead
21740 // (`a_refused_goaway_flush_waits_for_the_kernel_edge_then_closes`).
21741 assert!(
21742 connection.core.readiness.event.is_writable(),
21743 "with no refused write recorded, ensure_tls_flushed must re-signal \
21744 the WRITABLE event so the write path runs again"
21745 );
21746
21747 let mut received = Vec::new();
21748 assert!(
21749 flush_until_drained(
21750 &mut connection.socket,
21751 &mut client,
21752 &mut peer,
21753 &mut received
21754 ),
21755 "premise for the second arm: the records must actually drain once \
21756 the peer reads"
21757 );
21758
21759 assert!(
21760 matches!(connection.force_disconnect(), MuxResult::CloseSession),
21761 "with rustls holding nothing, the same call must close: delaying \
21762 further would strand a connection with no reason left to wait"
21763 );
21764 }
21765
21766 /// The `(H2State::Error, Position::Server)` arm of
21767 /// `ConnectionH2::dispatch_writable_state` reads the real handler's
21768 /// POST-flush answer: it keeps the session open while rustls still holds
21769 /// records — leaving WRITABLE to the kernel's edge, since the flush was
21770 /// refused — and closes once they are gone. The test drains the records
21771 /// itself between the two passes (`flush_until_drained`); the second pass
21772 /// only has to see that nothing is left.
21773 ///
21774 /// That arm has no flush of its own — `H2Shell::writable`'s preamble
21775 /// already issued this pass's `socket_write(&[])` and discarded both the
21776 /// size and the status it returned, and the `tls_wants_write` the arm is
21777 /// handed is the read after it, so that query is the only thing that can
21778 /// tell this site whether the flush landed. Until now the arm was
21779 /// exercised only as `h2_close::error_close_action`'s pure table; no test
21780 /// reached it through a connection whose handler could answer `true`,
21781 /// because none existed.
21782 ///
21783 /// TO SEE THIS RED: in that arm, replace
21784 /// `h2_close::error_close_action(tls_wants_write)` with
21785 /// `h2_close::error_close_action(false)` — the decision stops reading the
21786 /// socket and closes with records pending, sending FIN over plaintext the
21787 /// peer never received. Measured, this test fails on `a pass that finds
21788 /// records still pending after its own flush must not close the session:
21789 /// the close destroys plaintext the peer has not received and it reads the
21790 /// response as truncated`. Measured: `1116 passed; 1 failed` — it is the
21791 /// only test in the crate that moves.
21792 #[test]
21793 fn a_rustls_frontend_in_error_state_stays_open_until_its_records_drain() {
21794 let pool = make_pool_for_invariant_16();
21795 let (mut connection, mut peer, mut client) = rustls_h2_connection(&pool, H2State::Error);
21796 let mut context = test_context(&pool);
21797 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
21798 let body = queued_response_body();
21799
21800 let (absorbed, _status) = connection
21801 .socket
21802 .socket_write_vectored(&[IoSlice::new(body)]);
21803 assert!(
21804 absorbed > 0 && connection.socket.socket_wants_write(),
21805 "premise: the blocked kernel must leave real records pending, or \
21806 this arm reads its only input as false and the test proves nothing"
21807 );
21808
21809 connection.core.readiness.event.insert(Ready::WRITABLE);
21810 assert!(
21811 matches!(
21812 connection.writable(&mut context, EndpointClient(&mut router)),
21813 MuxResult::Continue
21814 ),
21815 "a pass that finds records still pending after its own flush must \
21816 not close the session: the close destroys plaintext the peer has \
21817 not received and it reads the response as truncated"
21818 );
21819 // The kernel refused the flush: it owes this socket its next
21820 // WRITABLE edge, so no synthetic one is queued (sozu-proxy/sozu#1788).
21821 assert!(
21822 connection.core.readiness.interest.is_writable(),
21823 "the delayed close must keep WRITABLE interest so the kernel edge \
21824 is delivered"
21825 );
21826 assert!(
21827 !connection.core.readiness.event.is_writable(),
21828 "a flush the kernel refused must leave WRITABLE to the kernel's \
21829 next edge, got {:?}",
21830 connection.core.readiness
21831 );
21832
21833 let mut received = Vec::new();
21834 assert!(
21835 flush_until_drained(
21836 &mut connection.socket,
21837 &mut client,
21838 &mut peer,
21839 &mut received
21840 ),
21841 "premise for the second arm: the records must actually drain once \
21842 the peer reads"
21843 );
21844
21845 connection.core.readiness.event.insert(Ready::WRITABLE);
21846 assert!(
21847 matches!(
21848 connection.writable(&mut context, EndpointClient(&mut router)),
21849 MuxResult::CloseSession
21850 ),
21851 "with rustls holding nothing, the same pass must close: the bytes \
21852 are all in the kernel and waiting longer strands the session"
21853 );
21854 }
21855
21856 /// The `H2State::GoAway` arm of `ConnectionH2::dispatch_writable_state`
21857 /// and the `ConnectionH2::dispatch_writable_state_after_flush` that
21858 /// settles it — the triple whose own comment calls it the primary
21859 /// truncation vector under HAProxy chaining — over the production TLS
21860 /// handler: it keeps the session open while rustls still holds records,
21861 /// leaving WRITABLE to the kernel's edge, and disconnects once they are
21862 /// gone. As in the Error-arm test, the records are drained by the test
21863 /// between the passes, not by the second pass.
21864 ///
21865 /// This is the triple #1454 names. Of the three, `finalize_write`'s is
21866 /// driven by `a_blocked_rustls_frontend_delivers_every_queued_byte_across_passes`
21867 /// above and the Error arm by
21868 /// `a_rustls_frontend_in_error_state_stays_open_until_its_records_drain`; this
21869 /// one completes the set. The fourth `socket_write(&[])` site,
21870 /// `flush_output_buffer`, is deliberately not targeted here: it keeps the
21871 /// status its flush returned instead of re-querying, so it is not this
21872 /// shape and a test aimed at it would prove nothing about the vector.
21873 ///
21874 /// The state assertion is not decoration. Falling through this arm reaches
21875 /// `force_disconnect`, which has a record guard of its own and answers
21876 /// `MuxResult::Continue` too — so the result alone cannot tell a correct
21877 /// hold from a fall-through that only looks correct. `H2State::Error` is
21878 /// what the fall-through leaves behind, and nothing else sets it here.
21879 ///
21880 /// TO SEE THIS RED: in `ConnectionH2::dispatch_writable_state_after_flush`,
21881 /// replace the `h2_close::goaway_close_action(TlsFlushPhase::AfterFlush,
21882 /// false, tls_wants_write)` third argument with `false`. The arm
21883 /// stops reading whether the flush `H2Shell::writable` performed for
21884 /// it landed, falls through to
21885 /// `force_disconnect`, and this test fails on `a GoAway pass whose flush
21886 /// did not land must stay in GoAway`. Measured: `1115 passed; 2 failed` —
21887 /// this test and `a_flush_that_does_not_drain_keeps_the_connection_open`,
21888 /// the same arm with one witness over a modelled handler and one over the
21889 /// production one.
21890 #[test]
21891 fn a_rustls_frontend_in_goaway_stays_open_until_its_records_drain() {
21892 let pool = make_pool_for_invariant_16();
21893 let (mut connection, mut peer, mut client) = rustls_h2_connection(&pool, H2State::GoAway);
21894 let mut context = test_context(&pool);
21895 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
21896 let body = queued_response_body();
21897
21898 let (absorbed, _status) = connection
21899 .socket
21900 .socket_write_vectored(&[IoSlice::new(body)]);
21901 assert!(
21902 absorbed > 0 && connection.socket.socket_wants_write(),
21903 "premise: the blocked kernel must leave real records pending"
21904 );
21905 assert!(
21906 !connection.core.peer_gone_after_final_goaway(),
21907 "premise: the peer must still be live, or the decision \
21908 short-circuits on its first boolean and never reads the socket"
21909 );
21910
21911 connection.core.readiness.event.insert(Ready::WRITABLE);
21912 assert!(
21913 matches!(
21914 connection.writable(&mut context, EndpointClient(&mut router)),
21915 MuxResult::Continue
21916 ),
21917 "a GoAway pass that finds records still pending after its own \
21918 flush must not close: FIN here destroys plaintext the peer never \
21919 received"
21920 );
21921 assert!(
21922 matches!(connection.core.state, H2State::GoAway),
21923 "a GoAway pass whose flush did not land must stay in GoAway: \
21924 reaching Error means the arm fell through to force_disconnect \
21925 and its own record guard, not this one, kept the session alive"
21926 );
21927 // The kernel refused the flush: it owes this socket its next
21928 // WRITABLE edge, so no synthetic one is queued (sozu-proxy/sozu#1788).
21929 assert!(
21930 !connection.core.readiness.event.is_writable(),
21931 "a flush the kernel refused must leave WRITABLE to the kernel's \
21932 next edge, got {:?}",
21933 connection.core.readiness
21934 );
21935
21936 let mut received = Vec::new();
21937 assert!(
21938 flush_until_drained(
21939 &mut connection.socket,
21940 &mut client,
21941 &mut peer,
21942 &mut received
21943 ),
21944 "premise for the second arm: the records must actually drain once \
21945 the peer reads"
21946 );
21947
21948 connection.core.readiness.event.insert(Ready::WRITABLE);
21949 assert!(
21950 matches!(
21951 connection.writable(&mut context, EndpointClient(&mut router)),
21952 MuxResult::CloseSession
21953 ),
21954 "with rustls holding nothing, the same pass must disconnect: every \
21955 byte is in the kernel and waiting longer strands the session"
21956 );
21957 }
21958
21959 // ── #1498: a DRIVEN TLS close_notify over a real `FrontRustls` ─────
21960 //
21961 // `H2Shell::begin_tls_close` is the seam that reaches
21962 // `FrontRustls::socket_close`, which is
21963 // `rustls::ServerConnection::send_close_notify`. Replacing its body with
21964 // `{}` at 0dddedf2 left the WHOLE suite green — `sozu-lib` `1114 passed;
21965 // 0 failed`, and no `sozu-e2e` test reddened on the mutation either —
21966 // while the same mutation on either of its two siblings from the same
21967 // extraction reddens at once. Measured against this branch's own `1115
21968 // passed` baseline: `tls_wants_write() -> false` gives `1104 passed; 11
21969 // failed` and a no-op `flush_tls_records()` gives `1110 passed; 5
21970 // failed`, ten and four of those respectively being tests OTHER than this
21971 // one — which is the asymmetry the whole file turns on. #1484's
21972 // coverage audit reached the same conclusion from the other direction,
21973 // listing `initiate_close_notify` among the `socket_wants_write` branches
21974 // its six `H2Shell<FrontRustls>` tests could not reach, for want of a
21975 // driven close_notify.
21976 //
21977 // The hole is structural rather than an oversight. Every handler the suite
21978 // drives a close over — `mio::net::TcpStream` and
21979 // `BackpressuredTlsSocket` — inherits `SocketHandler::socket_close`'s EMPTY
21980 // trait default, so to all of them the real seam and a no-op are the same
21981 // thing. `FrontRustls` is the only implementation in the tree that
21982 // overrides it, which is why the test below has to build one.
21983 //
21984 // And the assertion has to be on the ALERT, not on a FIN. That distinction
21985 // is the whole point of the alert: rustls answers `Ok(0)` from its reader
21986 // for a stream closed WITH a close_notify and
21987 // `UnexpectedEof("peer closed connection without sending TLS
21988 // close_notify")` for one closed without, so only the alert lets a peer
21989 // tell a complete response from a connection cut mid-stream. A test that
21990 // asserted a FIN had arrived would pass against the no-op mutant, which
21991 // still closes the socket.
21992
21993 /// The frontend idle timeout on a server H2 connection makes the peer
21994 /// receive a TLS close_notify alert.
21995 ///
21996 /// The route is the production one and the whole of it:
21997 /// `Mux::timeout` -> `Mux::delay_close_for_frontend_flush` ->
21998 /// `Connection::initiate_close_notify` ->
21999 /// `H2Shell::initiate_close_notify` -> `begin_tls_close` ->
22000 /// `FrontRustls::socket_close` -> `send_close_notify`.
22001 ///
22002 /// `Mux::delay_close_for_frontend_flush` is the SOLE caller of
22003 /// `Connection::initiate_close_notify`, and it is itself called from four
22004 /// sites in `Mux::ready_inner` and this one in `Mux::timeout_inner`.
22005 /// `Mux::shutting_down` is not among them: it drives frontend I/O and can
22006 /// flush records the alert has already generated, but it never asks for
22007 /// one. The timeout is chosen over the four `ready_inner` sites only
22008 /// because they need a `dyn ProxySession` and a `dyn L7Proxy`; the seam
22009 /// under test is the same on all five.
22010 ///
22011 /// The flush is `Connection::flush_output_buffer` — the same call
22012 /// `Mux::shutting_down_inner` makes for exactly this purpose, on a
22013 /// connection whose output queue is empty so nothing but the alert reaches
22014 /// the wire. `FrontRustls::socket_write(&[])` takes its "flush pending TLS
22015 /// records even if no application data was written" block, which is what
22016 /// hands the record to `write_tls`.
22017 ///
22018 /// TO SEE THIS RED: replace the body of `H2Shell::begin_tls_close`
22019 /// with `{}`. rustls is then never asked for the alert, `tls_wants_write()`
22020 /// answers false, `initiate_close_notify` returns false and the flush has
22021 /// nothing to push, so this test fails on `the peer must decrypt a TLS
22022 /// close_notify alert`. Measured: `1114 passed; 1 failed` — this test and
22023 /// nothing else, which is what a hole this complete looks like once it has
22024 /// exactly one witness. Note `close_notify_sent` is still set by the
22025 /// mutant, so no assertion on that flag could discriminate — only the
22026 /// bytes the peer receives can.
22027 #[test]
22028 fn a_frontend_timeout_makes_the_peer_receive_a_tls_close_notify() {
22029 use std::io::Read as _;
22030
22031 use crate::{StateResult, protocol::SessionState};
22032
22033 let pool = make_pool_for_invariant_16();
22034 let (mut connection, mut peer, mut client) = rustls_h2_connection(&pool, H2State::Header);
22035 // `Mux::consume_timer_entry` re-validates the wheel entry against the
22036 // core's own deadline and returns `Continue` having done NOTHING when
22037 // that deadline is still in the future. `ConnectionH2::new` arms one a
22038 // full `timeout_duration` out, so move it into the past: this is the
22039 // idle deadline genuinely elapsing, which is what the event loop would
22040 // be reacting to.
22041 connection.core.timeout_deadline = Some(Instant::now() - Duration::from_secs(60));
22042
22043 let frontend_token = mio::Token(0);
22044 let mut mux = crate::protocol::mux::Mux {
22045 configured_frontend_timeout: Duration::from_secs(30),
22046 frontend_token,
22047 frontend: crate::protocol::mux::Connection::H2(connection),
22048 router: Router::new(Duration::from_secs(30), Duration::from_secs(30)),
22049 // No streams at all, so the per-stream loop leaves `should_close`
22050 // true and `should_write` false, which is the only combination
22051 // that reaches `delay_close_for_frontend_flush`.
22052 context: test_context(&pool),
22053 session_ulid: Ulid::generate(),
22054 timeouts: crate::protocol::mux::MuxTimeouts::new(
22055 crate::timer::TimeoutContainer::new_empty(Duration::from_secs(30)),
22056 ),
22057 backend_registry: crate::protocol::mux::BackendRegistry::default(),
22058 backends: Rc::default(),
22059 };
22060 let mut metrics = SessionMetrics::new(None);
22061
22062 {
22063 let crate::protocol::mux::Connection::H2(h2) = &mux.frontend else {
22064 unreachable!("the frontend was built as H2")
22065 };
22066 assert!(
22067 !h2.socket.socket_wants_write(),
22068 "premise: the fixture must start with rustls holding nothing, \
22069 or the records this test attributes to the alert could be the \
22070 handshake's"
22071 );
22072 }
22073 assert!(
22074 !client
22075 .process_new_packets()
22076 .expect("the peer must decrypt what the frontend wrote")
22077 .peer_has_closed(),
22078 "premise: the peer must not have seen a close before the timeout \
22079 runs, or the closing assertion is reading a state it inherited"
22080 );
22081
22082 let result = mux.timeout(frontend_token, &mut metrics);
22083
22084 let mut received = Vec::new();
22085 let mut peer_saw_close_notify = false;
22086 for _ in 0..MAX_DRIVE_TICKS {
22087 mux.frontend.flush_output_buffer();
22088 // Loopback delivery is softirq-driven, not synchronous with
22089 // `write_tls`: give it a slot rather than spinning the CPU.
22090 std::thread::yield_now();
22091 drain_peer(&mut client, &mut peer, &mut received);
22092 peer_saw_close_notify = client
22093 .process_new_packets()
22094 .expect("the peer must decrypt what the frontend wrote")
22095 .peer_has_closed();
22096 if peer_saw_close_notify {
22097 break;
22098 }
22099 }
22100
22101 assert!(
22102 peer_saw_close_notify,
22103 "the peer must decrypt a TLS close_notify alert: without it a \
22104 client cannot tell this orderly shutdown from a stream truncated \
22105 mid-response, and reports a protocol error instead of a clean end"
22106 );
22107 let mut sentinel = [0u8; 1];
22108 assert!(
22109 matches!(client.reader().read(&mut sentinel), Ok(0)),
22110 "the alert must be what ended the peer's stream, stated in \
22111 rustls's own vocabulary: its reader answers Ok(0) only for a \
22112 stream closed with a close_notify, UnexpectedEof for a bare FIN \
22113 and WouldBlock for neither — so this is the assertion a FIN \
22114 cannot satisfy"
22115 );
22116 assert!(
22117 received.is_empty(),
22118 "nothing but the alert may reach the peer on this path: decrypted \
22119 plaintext here would mean the connection also wrote a frame after \
22120 asking rustls to close, which RFC 8446 §6.1 has the peer discard \
22121 ({} bytes)",
22122 received.len()
22123 );
22124 assert_eq!(
22125 result,
22126 StateResult::Continue,
22127 "a timeout that queued an alert must keep the session alive until \
22128 the records drain: closing here sends FIN over the very bytes the \
22129 close_notify was generated to deliver"
22130 );
22131 }
22132
22133 // ── Connection-level idle deadline: what counts as liveness ────────
22134 //
22135 // LIFECYCLE §9 invariant 9 says the connection timer resets only on
22136 // application activity, and `ConnectionH2::poll_read_target` spells out
22137 // the reason in prose: a peer sending periodic PINGs must not prevent
22138 // timeout detection on a stuck session. The READ side honoured it. The
22139 // WRITE side did not, because `H2Shell::write_streams` armed at pass
22140 // entry and `writable()` reaches that function on every proxying-state
22141 // pass — including the pass whose only output was the PING or SETTINGS
22142 // acknowledgement `flush_pending_control_frames` had just drained. So the
22143 // rule held in one direction and was undone in the other, for exactly the
22144 // frame types it names (sozu-proxy/sozu#1489).
22145 //
22146 // Every test below pairs the negative with a POSITIVE leg on the same
22147 // connection. A test that only asserted "the deadline did not move" would
22148 // pass just as well against a build that never armed at all, which is the
22149 // opposite defect and a worse one: it turns the frontend timeout into an
22150 // unconditional session cap.
22151
22152 /// Long enough that the per-stream idle deadline — a separate mechanism,
22153 /// deliberately untouched here — cannot retire the registered stream part
22154 /// way through a measurement and change what the later rows are measuring.
22155 const LIVENESS_TIMEOUT: Duration = Duration::from_secs(600);
22156
22157 /// A server connection whose frontend timeout and per-stream idle cap are
22158 /// both [`LIVENESS_TIMEOUT`], with ONE registered stream holding no queued
22159 /// response yet, plus the pass-zero instant every assertion below is
22160 /// expressed against.
22161 ///
22162 /// The stream is registered directly rather than opened with a HEADERS
22163 /// frame, and it starts EMPTY on purpose: a stream with queued output
22164 /// would let the write pass that flushes a PING ACK also flush that
22165 /// output, and arm the deadline for a reason the negative leg is not
22166 /// trying to measure.
22167 fn liveness_fixture(
22168 pool: &Rc<RefCell<Pool>>,
22169 ) -> (
22170 H2Shell<BackpressuredTlsSocket>,
22171 Context<TestListener>,
22172 GlobalStreamId,
22173 Instant,
22174 std::net::TcpStream,
22175 ) {
22176 let (mut connection, peer) = connection_with_backpressure(pool, 0, 0, H2State::Header);
22177 let t0 = connection.core.now;
22178 connection.core.timeout_duration = LIVENESS_TIMEOUT;
22179 connection.core.stream_idle_timeout = LIVENESS_TIMEOUT;
22180 connection.core.timeout_deadline = t0.checked_add(LIVENESS_TIMEOUT);
22181 connection
22182 .core
22183 .stream_table
22184 .set_expect_read(Some((H2StreamId::Zero, 9)));
22185 let mut context = test_context(pool);
22186 let gid = context
22187 .create_stream(Ulid::generate(), 1 << 16)
22188 .expect("test context must create a stream");
22189 connection
22190 .core
22191 .stream_table
22192 .register(REGISTERED_STREAM_ID, gid, connection.core.now);
22193 (connection, context, gid, t0, peer)
22194 }
22195
22196 /// A non-ACK PING. `ConnectionH2::handle_ping_frame` answers it by
22197 /// serialising a PING ACK into `self.zero` and parking
22198 /// `expect_write(Some(H2StreamId::Zero))`, which is what gives the write
22199 /// path something to send.
22200 fn liveness_ping_frame() -> Vec<u8> {
22201 let mut frame = vec![0, 0, 8, 6, 0, 0, 0, 0, 0];
22202 frame.extend_from_slice(&[1, 2, 3, 4, 5, 6, 7, 8]);
22203 frame
22204 }
22205
22206 /// A non-ACK SETTINGS carrying no entry.
22207 /// `ConnectionH2::handle_settings_frame` answers it with
22208 /// `serializer::SETTINGS_ACKNOWLEDGEMENT`, parking the same way.
22209 fn liveness_settings_frame() -> Vec<u8> {
22210 vec![0, 0, 0, 4, 0, 0, 0, 0, 0]
22211 }
22212
22213 /// A connection-level WINDOW_UPDATE. The control frame that queues NO
22214 /// acknowledgement, and therefore the one the pre-image already handled
22215 /// correctly — it is here so the fix is shown not to have moved it.
22216 fn liveness_window_update_frame() -> Vec<u8> {
22217 let mut frame = vec![0, 0, 4, 8, 0, 0, 0, 0, 0];
22218 frame.extend_from_slice(&1024u32.to_be_bytes());
22219 frame
22220 }
22221
22222 /// A HEADERS frame opening `stream_id`, END_STREAM | END_HEADERS.
22223 fn liveness_headers_frame(stream_id: u32) -> Vec<u8> {
22224 let mut encoder = crate::protocol::mux::hpack::Encoder::new();
22225 let block = encoder.encode([
22226 (&b":method"[..], &b"GET"[..]),
22227 (&b":scheme"[..], &b"https"[..]),
22228 (&b":authority"[..], &b"example.com"[..]),
22229 (&b":path"[..], &b"/"[..]),
22230 ]);
22231 let mut frame = Vec::with_capacity(9 + block.len());
22232 frame.extend_from_slice(&(block.len() as u32).to_be_bytes()[1..]);
22233 frame.push(1);
22234 frame.push(parser::FLAG_END_STREAM | parser::FLAG_END_HEADERS);
22235 frame.extend_from_slice(&stream_id.to_be_bytes());
22236 frame.extend_from_slice(&block);
22237 frame
22238 }
22239
22240 /// Spin until `n` bytes the peer wrote have reached the connection's
22241 /// socket.
22242 ///
22243 /// Not belt-and-braces: without it the read loop below can quiesce on a
22244 /// `WouldBlock` that only means "not yet", and every assertion downstream
22245 /// then measures a frame that was never delivered — which is a test that
22246 /// passes for the wrong reason, in the direction of the negative legs.
22247 fn liveness_await_bytes(connection: &H2Shell<BackpressuredTlsSocket>, n: usize) {
22248 let mut buf = vec![0u8; n];
22249 for _ in 0..1_000_000 {
22250 if let Ok(got) = connection.socket.socket_ref().peek(&mut buf)
22251 && got >= n
22252 {
22253 return;
22254 }
22255 std::thread::yield_now();
22256 }
22257 panic!("the peer's {n} bytes never reached the connection socket");
22258 }
22259
22260 /// Deliver `frame` at `at` and run the READ half only, the way
22261 /// `Mux::ready_inner` runs it: `readable()` while `filter_interest()` is
22262 /// readable.
22263 ///
22264 /// Stopping before the write half is what lets a test assert the premise —
22265 /// that the acknowledgement really was queued — instead of inferring it
22266 /// from the deadline it is about to measure.
22267 fn liveness_read(
22268 connection: &mut H2Shell<BackpressuredTlsSocket>,
22269 context: &mut Context<TestListener>,
22270 router: &mut Router,
22271 peer: &mut std::net::TcpStream,
22272 at: Instant,
22273 frame: &[u8],
22274 ) {
22275 context.now = at;
22276 peer.write_all(frame).expect("loopback write must complete");
22277 peer.flush().expect("loopback flush must complete");
22278 liveness_await_bytes(connection, frame.len());
22279 connection.core.readiness.interest.insert(Ready::READABLE);
22280 connection.core.readiness.event.insert(Ready::READABLE);
22281 for _ in 0..64 {
22282 if !connection.core.readiness.filter_interest().is_readable() {
22283 let mut leftover = [0u8; 1];
22284 assert!(
22285 !matches!(connection.socket.socket_ref().peek(&mut leftover), Ok(1)),
22286 "the read half stopped with the frame still unread, so \
22287 nothing below is measuring the frame it names"
22288 );
22289 return;
22290 }
22291 connection.readable(context, EndpointClient(&mut *router));
22292 }
22293 panic!(
22294 "the read half never settled: {:?}",
22295 connection.core.readiness
22296 );
22297 }
22298
22299 /// Run the WRITE half at the clock the read half left: `writable()` while
22300 /// `filter_interest()` is writable. This is the pass that used to arm the
22301 /// frontend deadline unconditionally.
22302 fn liveness_write(
22303 connection: &mut H2Shell<BackpressuredTlsSocket>,
22304 context: &mut Context<TestListener>,
22305 router: &mut Router,
22306 ) {
22307 for _ in 0..64 {
22308 if !connection.core.readiness.filter_interest().is_writable() {
22309 return;
22310 }
22311 connection.writable(context, EndpointClient(&mut *router));
22312 }
22313 panic!(
22314 "the write half never settled: {:?}",
22315 connection.core.readiness
22316 );
22317 }
22318
22319 /// Queue `blocks` as the registered stream's response and drive ONE
22320 /// `writable()` pass at `at`. Returns the bytes that reached the socket
22321 /// across the whole connection's lifetime so far.
22322 fn liveness_flush_stream(
22323 connection: &mut H2Shell<BackpressuredTlsSocket>,
22324 context: &mut Context<TestListener>,
22325 router: &mut Router,
22326 gid: GlobalStreamId,
22327 at: Instant,
22328 blocks: &[&'static [u8]],
22329 ) -> usize {
22330 for block in blocks {
22331 context.streams[gid]
22332 .back
22333 .out
22334 .push_back(kawa::OutBlock::Store(kawa::Store::Static(block)));
22335 }
22336 context.now = at;
22337 connection.core.readiness.interest.insert(Ready::WRITABLE);
22338 connection.core.readiness.event.insert(Ready::WRITABLE);
22339 connection.writable(context, EndpointClient(&mut *router));
22340 context.streams[gid].metrics.bout
22341 }
22342
22343 /// The deadline a connection publishes, as an offset from pass zero, so a
22344 /// failure reads in the units the issue's table is written in.
22345 fn liveness_deadline(connection: &H2Shell<BackpressuredTlsSocket>, t0: Instant) -> Duration {
22346 connection
22347 .core
22348 .poll_timeout()
22349 .expect("a live H2 connection must publish a deadline")
22350 .saturating_duration_since(t0)
22351 }
22352
22353 /// A peer that sends nothing but PINGs must not hold a stuck session open.
22354 ///
22355 /// The PING is answered — `handle_ping_frame` parks the ACK on
22356 /// `expect_write` and arms WRITABLE — so the write path genuinely runs and
22357 /// genuinely has bytes to emit. What it must not do is read that as the
22358 /// peer being alive on a stream: the ACK is the proxy talking to itself.
22359 ///
22360 /// TO SEE THIS RED: put `self.arm_timeout();` back as the first statement
22361 /// of `H2Shell::write_streams`. Measured on that revert, this test
22362 /// fails on `a PING is answered but is not stream activity`, with
22363 /// `left: 610s, right: 600.01s` — the deadline follows the PING instead of
22364 /// staying where the request left it. It is one of FOUR failures, the
22365 /// siblings being the three tests below; every other test in the suite
22366 /// passes.
22367 ///
22368 /// Neutering the OTHER half instead — `if size > 0` to `if false` in
22369 /// `ConnectionH2::handle_write` — reddens this test too, on its positive
22370 /// leg (`left: 600.01s, right: 1130s`). That is the pair that makes it a
22371 /// measurement: neither a build that always arms nor a build that never
22372 /// arms can satisfy it.
22373 #[test]
22374 fn a_ping_only_peer_does_not_postpone_the_connection_deadline() {
22375 let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 8, 16384)));
22376 let (mut connection, mut context, gid, t0, mut peer) = liveness_fixture(&pool);
22377 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
22378
22379 assert_eq!(
22380 liveness_deadline(&connection, t0),
22381 LIVENESS_TIMEOUT,
22382 "premise: the connection starts one full timeout out from pass zero"
22383 );
22384
22385 // The request. The READ side already honoured the rule, and this leg
22386 // is the control for it: a build that stopped arming on HEADERS would
22387 // make every "did not move" assertion below true for the wrong reason.
22388 let request_at = Duration::from_millis(10);
22389 liveness_read(
22390 &mut connection,
22391 &mut context,
22392 &mut router,
22393 &mut peer,
22394 t0 + request_at,
22395 &liveness_headers_frame(3),
22396 );
22397 liveness_write(&mut connection, &mut context, &mut router);
22398 assert_eq!(
22399 liveness_deadline(&connection, t0),
22400 request_at + LIVENESS_TIMEOUT,
22401 "HEADERS is application activity and must arm the deadline: \
22402 without this leg the assertions below cannot tell a fix from a \
22403 build that never arms at all"
22404 );
22405
22406 // The defect. Two PINGs, 490 seconds apart, either of which used to
22407 // renew the session on its own.
22408 for ping_at in [Duration::from_secs(10), Duration::from_secs(500)] {
22409 liveness_read(
22410 &mut connection,
22411 &mut context,
22412 &mut router,
22413 &mut peer,
22414 t0 + ping_at,
22415 &liveness_ping_frame(),
22416 );
22417 assert!(
22418 !connection.core.output.is_empty(),
22419 "premise: the PING must really have been answered and queued, \
22420 so the write pass below is the ACK-only pass this test is \
22421 about and not a no-op"
22422 );
22423
22424 liveness_write(&mut connection, &mut context, &mut router);
22425
22426 assert!(
22427 connection.core.output.is_empty(),
22428 "premise: the ACK flush must have completed, which is what \
22429 lets `flush_pending_control_frames` answer Proceed and fall \
22430 through into the proxying-state write pass"
22431 );
22432 assert_eq!(
22433 liveness_deadline(&connection, t0),
22434 request_at + LIVENESS_TIMEOUT,
22435 "a PING is answered but is not stream activity: the deadline \
22436 must stay where the request left it, or a peer sending \
22437 periodic PINGs holds a stuck session open indefinitely"
22438 );
22439 }
22440
22441 // A WINDOW_UPDATE queues no acknowledgement, so the pre-image already
22442 // left it alone. Pinned so the fix is shown not to have moved it.
22443 liveness_read(
22444 &mut connection,
22445 &mut context,
22446 &mut router,
22447 &mut peer,
22448 t0 + Duration::from_secs(520),
22449 &liveness_window_update_frame(),
22450 );
22451 liveness_write(&mut connection, &mut context, &mut router);
22452 assert_eq!(
22453 liveness_deadline(&connection, t0),
22454 request_at + LIVENESS_TIMEOUT,
22455 "a WINDOW_UPDATE queues nothing and must not arm the deadline"
22456 );
22457
22458 // The positive leg, on the same connection: outbound application data
22459 // IS activity. Without it every assertion above is satisfied by a
22460 // build that never arms the deadline at all.
22461 let response_at = Duration::from_secs(530);
22462 let written = liveness_flush_stream(
22463 &mut connection,
22464 &mut context,
22465 &mut router,
22466 gid,
22467 t0 + response_at,
22468 &[FIRST_BLOCK, SECOND_BLOCK],
22469 );
22470 assert_eq!(
22471 written, TOTAL_QUEUED,
22472 "premise for the positive leg: the pass must really have moved the \
22473 response bytes, or the deadline below moved without stream data"
22474 );
22475 assert_eq!(
22476 liveness_deadline(&connection, t0),
22477 response_at + LIVENESS_TIMEOUT,
22478 "a write pass that moved a stream's bytes IS liveness and must arm \
22479 the deadline: the fix withdraws arming from acknowledgement-only \
22480 passes, not from the write path"
22481 );
22482 }
22483
22484 /// The same contract for SETTINGS, the other control frame that queues an
22485 /// acknowledgement.
22486 ///
22487 /// Not a duplicate of the PING test: `handle_settings_frame` reaches the
22488 /// park through an entirely different body — the per-identifier apply
22489 /// loop, the `pending_table_size_update` signal and
22490 /// `serializer::SETTINGS_ACKNOWLEDGEMENT` rather than
22491 /// `serializer::gen_ping_acknowledgement` — and the issue's table measured
22492 /// both moving independently.
22493 ///
22494 /// TO SEE THIS RED: put `self.arm_timeout();` back as the first statement
22495 /// of `H2Shell::write_streams`. Measured on that revert, this test
22496 /// fails on `a SETTINGS ACK is not stream activity`, with
22497 /// `left: 640s, right: 600.01s`. Neutering the other half instead —
22498 /// `if size > 0` to `if false` in `ConnectionH2::handle_write` — reddens
22499 /// it on its positive leg, `left: 600.01s, right: 650s`.
22500 #[test]
22501 fn a_settings_only_peer_does_not_postpone_the_connection_deadline() {
22502 let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 8, 16384)));
22503 let (mut connection, mut context, gid, t0, mut peer) = liveness_fixture(&pool);
22504 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
22505
22506 let request_at = Duration::from_millis(10);
22507 liveness_read(
22508 &mut connection,
22509 &mut context,
22510 &mut router,
22511 &mut peer,
22512 t0 + request_at,
22513 &liveness_headers_frame(3),
22514 );
22515 liveness_write(&mut connection, &mut context, &mut router);
22516 assert_eq!(
22517 liveness_deadline(&connection, t0),
22518 request_at + LIVENESS_TIMEOUT,
22519 "premise: the request armed the deadline, so a later move is \
22520 attributable to the frame that caused it"
22521 );
22522
22523 liveness_read(
22524 &mut connection,
22525 &mut context,
22526 &mut router,
22527 &mut peer,
22528 t0 + Duration::from_secs(40),
22529 &liveness_settings_frame(),
22530 );
22531 assert!(
22532 !connection.core.output.is_empty(),
22533 "premise: the SETTINGS must really have been acknowledged and \
22534 queued, so the write pass below is the ACK-only pass"
22535 );
22536
22537 liveness_write(&mut connection, &mut context, &mut router);
22538
22539 assert!(
22540 connection.core.output.is_empty(),
22541 "premise: the ACK flush must have completed and fallen through \
22542 into the proxying-state write pass"
22543 );
22544 assert_eq!(
22545 liveness_deadline(&connection, t0),
22546 request_at + LIVENESS_TIMEOUT,
22547 "a SETTINGS ACK is not stream activity: the deadline must stay \
22548 where the request left it"
22549 );
22550
22551 let response_at = Duration::from_secs(50);
22552 let written = liveness_flush_stream(
22553 &mut connection,
22554 &mut context,
22555 &mut router,
22556 gid,
22557 t0 + response_at,
22558 &[FIRST_BLOCK, SECOND_BLOCK],
22559 );
22560 assert_eq!(
22561 written, TOTAL_QUEUED,
22562 "premise for the positive leg: the pass must really have moved the \
22563 response bytes"
22564 );
22565 assert_eq!(
22566 liveness_deadline(&connection, t0),
22567 response_at + LIVENESS_TIMEOUT,
22568 "outbound application data must still arm the deadline"
22569 );
22570 }
22571
22572 /// A write pass that emits a stream's bytes arms the deadline; the pass
22573 /// before it, which had nothing queued, does not.
22574 ///
22575 /// The PING and SETTINGS tests above each carry a positive leg, so this
22576 /// one is not the only guard against a build that never arms. What it adds
22577 /// is the pair measured on ONE connection with NOTHING but the queued
22578 /// response changing between the two passes: no frame is read, so neither
22579 /// `handle_headers_frame` nor `poll_read_target`'s DATA arm can be what
22580 /// moved the deadline, and the write path is the only remaining candidate.
22581 ///
22582 /// TO SEE THIS RED: change the `if size > 0` gate in
22583 /// `ConnectionH2::handle_write` to `if false`. The empty-pass assertion
22584 /// still holds and this test then fails on `a write pass that moved a
22585 /// stream's bytes must arm the deadline`, with `left: 600s, right: 620s`.
22586 /// Restoring `self.arm_timeout()` to the top of
22587 /// `H2Shell::write_streams` reddens the OTHER assertion instead, `a
22588 /// write pass that emitted nothing is not liveness`, with
22589 /// `left: 610s, right: 600s`.
22590 #[test]
22591 fn a_write_pass_that_moves_stream_bytes_arms_the_connection_deadline() {
22592 let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 8, 16384)));
22593 let (mut connection, mut context, gid, t0, _peer) = liveness_fixture(&pool);
22594 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
22595
22596 // A write pass with nothing queued: the control that makes the
22597 // assertion below a measurement of the bytes and not of the pass.
22598 let empty_at = Duration::from_secs(10);
22599 let written = liveness_flush_stream(
22600 &mut connection,
22601 &mut context,
22602 &mut router,
22603 gid,
22604 t0 + empty_at,
22605 &[],
22606 );
22607 assert_eq!(
22608 written, 0,
22609 "premise: a pass with nothing queued moves no byte"
22610 );
22611 assert_eq!(
22612 liveness_deadline(&connection, t0),
22613 LIVENESS_TIMEOUT,
22614 "a write pass that emitted nothing is not liveness"
22615 );
22616
22617 let response_at = Duration::from_secs(20);
22618 let written = liveness_flush_stream(
22619 &mut connection,
22620 &mut context,
22621 &mut router,
22622 gid,
22623 t0 + response_at,
22624 &[FIRST_BLOCK, SECOND_BLOCK],
22625 );
22626 assert_eq!(
22627 written, TOTAL_QUEUED,
22628 "premise: the pass must really have moved the response bytes"
22629 );
22630 assert_eq!(
22631 liveness_deadline(&connection, t0),
22632 response_at + LIVENESS_TIMEOUT,
22633 "a write pass that moved a stream's bytes must arm the deadline"
22634 );
22635 }
22636
22637 /// Draining changes nothing about what counts as liveness.
22638 ///
22639 /// `Mux::drive_frontend_shutdown_io` sets `force_h2_write` unconditionally
22640 /// for an H2 frontend, so a draining connection still in `H2State::Header`
22641 /// reaches `H2Shell::write_streams` on every `shutting_down()` poll —
22642 /// which is why arming at pass entry made a draining session immune to the
22643 /// frontend timeout for the whole drain, whether or not a byte moved. The
22644 /// drain keeps its deadline the same way every other pass does: by
22645 /// flushing a stream's bytes.
22646 ///
22647 /// TO SEE THIS RED: change the `if size > 0` gate in
22648 /// `ConnectionH2::handle_write` to `if false`. This test then fails on `a
22649 /// draining pass that flushed a stream's bytes must arm the deadline`,
22650 /// with `left: 600s, right: 620s`. Restoring `self.arm_timeout()` to the
22651 /// top of `H2Shell::write_streams` reddens the other assertion, `a
22652 /// draining pass that emitted nothing is not liveness either`, with
22653 /// `left: 610s, right: 600s`.
22654 #[test]
22655 fn a_draining_write_pass_that_moves_stream_bytes_still_arms_the_deadline() {
22656 let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 8, 16384)));
22657 let (mut connection, mut context, gid, t0, _peer) = liveness_fixture(&pool);
22658 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
22659 connection.core.drain.__test_set_draining();
22660
22661 let empty_at = Duration::from_secs(10);
22662 let written = liveness_flush_stream(
22663 &mut connection,
22664 &mut context,
22665 &mut router,
22666 gid,
22667 t0 + empty_at,
22668 &[],
22669 );
22670 assert_eq!(
22671 written, 0,
22672 "premise: a draining pass with nothing queued moves no byte"
22673 );
22674 assert_eq!(
22675 liveness_deadline(&connection, t0),
22676 LIVENESS_TIMEOUT,
22677 "a draining pass that emitted nothing is not liveness either: \
22678 `drive_frontend_shutdown_io` forces a write pass on every \
22679 shutting-down poll, so arming here renews the session for free"
22680 );
22681
22682 let response_at = Duration::from_secs(20);
22683 let written = liveness_flush_stream(
22684 &mut connection,
22685 &mut context,
22686 &mut router,
22687 gid,
22688 t0 + response_at,
22689 &[FIRST_BLOCK, SECOND_BLOCK],
22690 );
22691 assert_eq!(
22692 written, TOTAL_QUEUED,
22693 "premise: the draining pass must really have moved the response \
22694 bytes, so the drain has time to finish delivering them"
22695 );
22696 assert_eq!(
22697 liveness_deadline(&connection, t0),
22698 response_at + LIVENESS_TIMEOUT,
22699 "a draining pass that flushed a stream's bytes must arm the \
22700 deadline: the drain has to be able to finish delivering a \
22701 response the peer is still reading"
22702 );
22703 }
22704 // ── LIFECYCLE §9 invariant 14 — the backend accounting ledger ────────
22705 //
22706 // Invariant 14 is a balance: every stream removed outside
22707 // `Connection::end_stream` must release the `active_requests` charge its
22708 // creation took. Before #1340's Question 12 that balance was spread over
22709 // six sites, each reaching a `Rc<RefCell<Backend>>` through
22710 // `Position::Client` and mutating it in place, and nothing could observe
22711 // the pair — a missed decrement showed up as load-balancing drift in
22712 // production, days later.
22713 //
22714 // The core now emits `BackendDelta`s and the embedder performs them, so
22715 // the balance is a property of one observable list. These tests read that
22716 // list directly, and then apply it to a real `Backend` and assert the
22717 // counter returns to where it started, which is the invariant itself
22718 // rather than a proxy for it.
22719
22720 /// Sum a ledger into the net `active_requests` change it describes, and
22721 /// the number of connection releases it carries.
22722 fn reconcile(deltas: &[BackendDelta]) -> (i64, usize) {
22723 let mut net = 0i64;
22724 let mut closed = 0usize;
22725 for delta in deltas {
22726 match delta.change {
22727 BackendChange::StreamsStarted(count) => net += count as i64,
22728 BackendChange::StreamsEnded(count) => net -= count as i64,
22729 BackendChange::ConnectionClosed => closed += 1,
22730 // Only `Mux::dial_backend` emits it, and no scenario here
22731 // dials: an H2 connection's own paths never release a
22732 // reservation.
22733 BackendChange::DialFailed(_) => {
22734 unreachable!("no invariant-14 scenario dials a backend")
22735 }
22736 }
22737 }
22738 (net, closed)
22739 }
22740
22741 /// Everything one of these scenarios needs: a live backend connection in
22742 /// `Connected`, the registry entry behind it, and the peer that keeps the
22743 /// loopback socket open.
22744 struct LedgerFixture {
22745 connection: Connection<SessionTcpStream>,
22746 registry: crate::protocol::mux::BackendRegistry,
22747 backend: Rc<RefCell<crate::backends::Backend>>,
22748 _peer: std::net::TcpStream,
22749 }
22750
22751 fn ledger_fixture(pool: &Rc<RefCell<Pool>>) -> LedgerFixture {
22752 let (socket, peer) = connected_socket();
22753 let address: std::net::SocketAddr =
22754 "127.0.0.1:2".parse().expect("backend address must parse");
22755 let backend = Rc::new(RefCell::new(crate::backends::Backend::new(
22756 "ledger-backend",
22757 address,
22758 None,
22759 None,
22760 None,
22761 )));
22762 let mut registry = crate::protocol::mux::BackendRegistry::default();
22763 let backend_id = registry.id_for(&backend);
22764 let session_ulid = Ulid::generate();
22765 let mut connection = Connection::new_h2_client(
22766 session_ulid,
22767 SessionTcpStream::new(socket, session_ulid, Some(address)),
22768 "ledger-cluster".into(),
22769 backend_id,
22770 &mut PoolBufferSource::new(Rc::downgrade(pool)),
22771 Duration::from_secs(30),
22772 H2FloodConfig::default(),
22773 H2ConnectionConfig::default(),
22774 Duration::from_secs(30),
22775 None,
22776 )
22777 .expect("the test pool must hand out a buffer for the H2 backend");
22778 // `new_h2_client` opens in `Connecting`, where the charge is taken by
22779 // the embedder at the transition rather than by `start_stream`. Move
22780 // it the way a completed dial does, so these scenarios exercise the
22781 // `Connected` guard the three reset paths read.
22782 if let Position::Client(_, _, status) = connection.position_mut() {
22783 *status = BackendStatus::Connected;
22784 }
22785 LedgerFixture {
22786 connection,
22787 registry,
22788 backend,
22789 _peer: peer,
22790 }
22791 }
22792
22793 /// Open `count` streams on the backend through the production path, and
22794 /// return their global ids.
22795 fn open_streams(
22796 fixture: &mut LedgerFixture,
22797 context: &mut Context<TestListener>,
22798 count: usize,
22799 ) -> Vec<GlobalStreamId> {
22800 (0..count)
22801 .map(|_| {
22802 let gid = context
22803 .create_stream(Ulid::generate(), 65_535)
22804 .expect("the test pool must hand out stream buffers");
22805 assert!(
22806 fixture.connection.start_stream(gid, context),
22807 "the backend must accept the stream this scenario opens"
22808 );
22809 gid
22810 })
22811 .collect()
22812 }
22813
22814 /// Apply a ledger and read the counter it leaves behind.
22815 fn settle(fixture: &mut LedgerFixture, context: &mut Context<TestListener>) -> usize {
22816 let backends = RefCell::new(crate::backends::BackendMap::with_seed(0));
22817 for delta in std::mem::take(&mut context.backend_deltas) {
22818 fixture.registry.apply(delta, &backends);
22819 }
22820 fixture.backend.borrow().active_requests
22821 }
22822
22823 /// The nominal path: what `Connection::start_stream` charges,
22824 /// `Connection::end_stream` releases, and the close releases the
22825 /// connection slot exactly once.
22826 #[test]
22827 fn invariant_14_start_and_end_stream_balance() {
22828 let pool = make_pool_for_invariant_16();
22829 let mut context = test_context(&pool);
22830 let mut fixture = ledger_fixture(&pool);
22831 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
22832
22833 let gids = open_streams(&mut fixture, &mut context, 3);
22834 assert_eq!(
22835 reconcile(&context.backend_deltas),
22836 (3, 0),
22837 "three started streams must charge exactly three"
22838 );
22839
22840 for gid in gids {
22841 fixture.connection.end_stream(gid, &mut context);
22842 }
22843 fixture
22844 .connection
22845 .close(&mut context, EndpointClient(&mut router));
22846
22847 assert_eq!(
22848 reconcile(&context.backend_deltas),
22849 (0, 1),
22850 "every charge must be released, and the connection released once"
22851 );
22852 assert_eq!(
22853 settle(&mut fixture, &mut context),
22854 0,
22855 "applying the ledger must return active_requests to zero"
22856 );
22857 }
22858
22859 /// A refused `start_stream` must charge nothing at all — not charge and
22860 /// then correctly undo. The rollback that used to sit here is the shape
22861 /// this asserts is gone: a ledger with a `+1` and a `-1` in it would also
22862 /// reconcile to zero, so the assertion is on the list being EMPTY.
22863 #[test]
22864 fn invariant_14_a_refused_start_stream_charges_nothing() {
22865 let pool = make_pool_for_invariant_16();
22866 let mut context = test_context(&pool);
22867 let mut fixture = ledger_fixture(&pool);
22868
22869 // Drive the peer's MAX_CONCURRENT_STREAMS to zero so the very next
22870 // `start_stream` is refused by RFC 9113 §5.1.2's check.
22871 let Connection::H2(shell) = &mut fixture.connection else {
22872 unreachable!("the fixture was built as H2");
22873 };
22874 shell.core.peer_settings.settings_max_concurrent_streams = 0;
22875
22876 let gid = context
22877 .create_stream(Ulid::generate(), 65_535)
22878 .expect("the test pool must hand out stream buffers");
22879 assert!(
22880 !fixture.connection.start_stream(gid, &mut context),
22881 "premise: the backend must refuse this stream"
22882 );
22883
22884 assert!(
22885 context.backend_deltas.is_empty(),
22886 "a refused start_stream must leave no entry in the ledger at all, \
22887 not a charge and a compensating release: got {:?}",
22888 context.backend_deltas
22889 );
22890 assert_eq!(
22891 settle(&mut fixture, &mut context),
22892 0,
22893 "a refused start_stream must leave active_requests untouched"
22894 );
22895 }
22896
22897 /// An inbound `RST_STREAM` removes the stream without going through
22898 /// `Connection::end_stream`, so it owes the release itself.
22899 #[test]
22900 fn invariant_14_an_inbound_rst_stream_releases_its_charge() {
22901 let pool = make_pool_for_invariant_16();
22902 let mut context = test_context(&pool);
22903 let mut fixture = ledger_fixture(&pool);
22904 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
22905
22906 let _gids = open_streams(&mut fixture, &mut context, 2);
22907 let Connection::H2(shell) = &mut fixture.connection else {
22908 unreachable!("the fixture was built as H2");
22909 };
22910 let reset_id = *shell
22911 .core
22912 .stream_table
22913 .streams()
22914 .keys()
22915 .next()
22916 .expect("the scenario opened two streams");
22917 shell.core.handle_rst_stream_frame(
22918 parser::RstStream {
22919 stream_id: reset_id,
22920 error_code: H2Error::Cancel as u32,
22921 },
22922 &mut context,
22923 EndpointClient(&mut router),
22924 );
22925
22926 assert_eq!(
22927 reconcile(&context.backend_deltas),
22928 (1, 0),
22929 "the reset stream must release its charge, and only its own"
22930 );
22931 assert_eq!(
22932 settle(&mut fixture, &mut context),
22933 1,
22934 "one of the two streams is gone, so exactly one charge remains"
22935 );
22936 }
22937
22938 /// On a backend connection (`Position::Client`) a `RST_STREAM` the backend
22939 /// sends before its response starts — typically `REFUSED_STREAM` under
22940 /// load — is not the Rapid Reset signature: Sōzu opened the stream and the
22941 /// reset costs it nothing. Three hundred of them, sixty at a time, must
22942 /// not trip the pre-response reset cap.
22943 ///
22944 /// TO SEE THIS RED: in `ConnectionH2::handle_rst_stream_frame`, drop the
22945 /// `self.position.is_client()` term from `response_started`. Every reset
22946 /// then counts as pre-response, and with one reset per opened stream the
22947 /// 251st crosses `h2_max_rst_stream_abusive_lifetime`.
22948 #[test]
22949 fn backend_resets_before_the_response_are_not_rapid_reset() {
22950 const ROUNDS: usize = 5;
22951 const PER_ROUND: usize = 60;
22952 let pool = Rc::new(RefCell::new(Pool::with_capacity(
22953 4,
22954 2 * ROUNDS * PER_ROUND + 8,
22955 16_384,
22956 )));
22957 let mut context = test_context(&pool);
22958 let mut fixture = ledger_fixture(&pool);
22959 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
22960
22961 for round in 0..ROUNDS {
22962 let _gids = open_streams(&mut fixture, &mut context, PER_ROUND);
22963 let Connection::H2(shell) = &mut fixture.connection else {
22964 unreachable!("the fixture was built as H2");
22965 };
22966 let open: Vec<StreamId> = shell.core.stream_table.streams().keys().copied().collect();
22967 assert_eq!(open.len(), PER_ROUND, "round {round}: premise");
22968 for stream_id in open {
22969 let _ = shell.core.handle_rst_stream_frame(
22970 parser::RstStream {
22971 stream_id,
22972 error_code: H2Error::RefusedStream as u32,
22973 },
22974 &mut context,
22975 EndpointClient(&mut router),
22976 );
22977 assert!(
22978 !matches!(shell.core.state, H2State::GoAway | H2State::Error),
22979 "round {round}: backend reset of stream {stream_id} must not send GOAWAY"
22980 );
22981 }
22982 }
22983 }
22984
22985 // ── RST caps: what a frontend reset counts toward ───────────────────
22986
22987 /// A frontend H2 connection whose received-RST lifetime cap has the floor
22988 /// `lifetime_floor`, every other threshold at its default, so the
22989 /// scenarios below reach the cap in a handful of resets instead of
22990 /// 200 000.
22991 fn frontend_rst_fixture(
22992 lifetime_floor: u64,
22993 ) -> (
22994 Rc<RefCell<Pool>>,
22995 H2Shell<mio::net::TcpStream>,
22996 std::net::TcpStream,
22997 Context<TestListener>,
22998 Router,
22999 ) {
23000 let pool = Rc::new(RefCell::new(Pool::with_capacity(8, 512, 16_384)));
23001 let (mut connection, peer) = test_h2_connection(&pool, None);
23002 let config = H2FloodConfig::from_optional(
23003 None,
23004 None,
23005 None,
23006 None,
23007 None,
23008 None,
23009 None,
23010 Some(lifetime_floor),
23011 None,
23012 None,
23013 None,
23014 None,
23015 None,
23016 None,
23017 );
23018 connection.core.flood_detector =
23019 h2_flood_detector::H2FloodDetector::new(config, connection.core.now);
23020 let context = test_context(&pool);
23021 let router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
23022 (pool, connection, peer, context, router)
23023 }
23024
23025 /// Register client stream `stream_id` on `connection` as a stream routed
23026 /// to a backend, with its response started or not.
23027 fn open_routed_frontend_stream(
23028 connection: &mut H2Shell<mio::net::TcpStream>,
23029 context: &mut Context<TestListener>,
23030 stream_id: StreamId,
23031 response_started: bool,
23032 ) {
23033 let gid = context
23034 .create_stream(Ulid::generate(), 65_535)
23035 .expect("the test pool must hand out stream buffers");
23036 let stream = &mut context.streams[gid];
23037 stream.context.backend_id = Some(Rc::from("probe-backend"));
23038 if response_started {
23039 stream.back.parsing_phase = kawa::ParsingPhase::Body;
23040 }
23041 let now = connection.core.now;
23042 connection.core.stream_table.register(stream_id, gid, now);
23043 }
23044
23045 /// Feed one client `RST_STREAM(CANCEL)` for `stream_id` through
23046 /// `handle_rst_stream_frame`, and report whether the connection is now
23047 /// going away.
23048 fn client_cancels(
23049 connection: &mut H2Shell<mio::net::TcpStream>,
23050 context: &mut Context<TestListener>,
23051 router: &mut Router,
23052 stream_id: StreamId,
23053 ) -> bool {
23054 let _ = connection.core.handle_rst_stream_frame(
23055 parser::RstStream {
23056 stream_id,
23057 error_code: H2Error::Cancel as u32,
23058 },
23059 context,
23060 EndpointClient(router),
23061 );
23062 matches!(connection.core.state, H2State::GoAway | H2State::Error)
23063 }
23064
23065 /// A client that cancels every stream after its response started — a
23066 /// gRPC client abandoning server streams, a browser leaving a page while
23067 /// downloads run — resets exactly as many streams as a backend answered.
23068 /// The received-RST lifetime cap must never trip on it.
23069 ///
23070 /// TO SEE THIS RED: in `ConnectionH2::handle_rst_stream_frame`, count the
23071 /// reset stream with `record_stream_opened` after `record_rst_lifetime`
23072 /// instead of before. Each check then sees one more reset than answered
23073 /// streams, and the reset just past the floor sends GOAWAY.
23074 #[test]
23075 fn resets_after_the_response_never_trip_the_lifetime_cap() {
23076 let (_pool, mut connection, _peer, mut context, mut router) = frontend_rst_fixture(4);
23077 for n in 0..64u32 {
23078 let stream_id = 2 * n + 1;
23079 open_routed_frontend_stream(&mut connection, &mut context, stream_id, true);
23080 assert!(
23081 !client_cancels(&mut connection, &mut context, &mut router, stream_id),
23082 "reset {} of a stream whose response started must not send GOAWAY",
23083 n + 1
23084 );
23085 }
23086 }
23087
23088 /// A frontend connection whose pre-response cap has the floor `floor`,
23089 /// every other threshold at its default (soft refusal at 50 %).
23090 fn frontend_pre_response_fixture(
23091 floor: u64,
23092 ) -> (
23093 Rc<RefCell<Pool>>,
23094 H2Shell<mio::net::TcpStream>,
23095 std::net::TcpStream,
23096 Context<TestListener>,
23097 Router,
23098 ) {
23099 let pool = Rc::new(RefCell::new(Pool::with_capacity(8, 512, 16_384)));
23100 let (mut connection, peer) = test_h2_connection(&pool, None);
23101 let config = H2FloodConfig::from_optional(
23102 None,
23103 None,
23104 None,
23105 None,
23106 None,
23107 None,
23108 None,
23109 None,
23110 Some(floor),
23111 None,
23112 None,
23113 None,
23114 None,
23115 None,
23116 );
23117 connection.core.flood_detector =
23118 h2_flood_detector::H2FloodDetector::new(config, connection.core.now);
23119 let context = test_context(&pool);
23120 let router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
23121 (pool, connection, peer, context, router)
23122 }
23123
23124 /// A client that ignores `REFUSED_STREAM` and resets the streams Sōzu
23125 /// refused under flood pressure still reaches the pre-response cap: a
23126 /// refused stream never got a response, so its reset is a pre-response
23127 /// one even though the stream was never registered. A reset of a stream
23128 /// that is simply gone keeps counting as response-started.
23129 ///
23130 /// TO SEE THIS RED: in `ConnectionH2::handle_rst_stream_frame`, delete
23131 /// the `flood_refused_streams` arm. The resets then count as
23132 /// response-started, the pre-response count stops at the soft threshold,
23133 /// and no GOAWAY is ever sent.
23134 #[test]
23135 fn resets_of_streams_refused_under_flood_count_as_pre_response() {
23136 const FLOOR: u64 = 10;
23137 let (_pool, mut connection, _peer, mut context, mut router) =
23138 frontend_pre_response_fixture(FLOOR);
23139 let refused = (FLOOR as u32 + 1) * 2;
23140 for n in 0..refused {
23141 let stream_id = 2 * n + 1;
23142 connection
23143 .core
23144 .stream_table
23145 .observe_peer_stream_id(stream_id);
23146 connection.core.note_flood_refused_stream(stream_id);
23147 }
23148 assert_eq!(
23149 connection.core.flood_refused_streams,
23150 Some((1, 2 * refused - 1)),
23151 "consecutive refusals form one run"
23152 );
23153 // Resets of ids past the run: gone, not refused, never pre-response,
23154 // and no reason to stop refusing.
23155 for n in 0..=FLOOR as u32 {
23156 let stream_id = 2 * (refused + n) + 1;
23157 assert!(
23158 !client_cancels(&mut connection, &mut context, &mut router, stream_id),
23159 "a reset of a stream outside the refused run must not count as pre-response"
23160 );
23161 }
23162 assert!(!connection.core.flood_refusal_ignored);
23163 let mut tripped_at = None;
23164 for n in 0..refused {
23165 if client_cancels(&mut connection, &mut context, &mut router, 2 * n + 1) {
23166 tripped_at = Some(u64::from(n) + 1);
23167 break;
23168 }
23169 }
23170 assert_eq!(
23171 tripped_at,
23172 Some(FLOOR + 1),
23173 "resets of refused streams must reach the pre-response cap just past its floor"
23174 );
23175 assert!(
23176 connection.core.flood_refusal_ignored,
23177 "a client that resets a refused stream gets no more refusals"
23178 );
23179 }
23180
23181 /// An accepted stream ends the refused run; the next refusal starts a new
23182 /// one, so a stream that was served never counts as refused.
23183 #[test]
23184 fn an_accepted_stream_ends_the_flood_refused_run() {
23185 let (_pool, mut connection, _peer, mut context, _router) =
23186 frontend_pre_response_fixture(10);
23187 connection.core.note_flood_refused_stream(1);
23188 connection.core.note_flood_refused_stream(3);
23189 assert_eq!(connection.core.flood_refused_streams, Some((1, 3)));
23190 connection
23191 .core
23192 .create_stream(5, &mut context)
23193 .expect("the pool must hand out stream buffers");
23194 connection.core.note_flood_refused_stream(7);
23195 assert_eq!(
23196 connection.core.flood_refused_streams,
23197 Some((7, 7)),
23198 "a refusal after an accepted stream starts a new run"
23199 );
23200 }
23201
23202 /// A frontend connection over a [`PacedSocket`] with streams 1 and 3
23203 /// open, a pre-response floor of 10 and a glitch limit of `glitch_limit`,
23204 /// its SETTINGS acknowledged, and `pre_response_resets` pre-response
23205 /// resets on record. Six or more put it in the soft state: past half the
23206 /// floor, more than the streams it got answered (none).
23207 fn soft_refusal_connection(
23208 glitch_limit: Option<u32>,
23209 pre_response_resets: u32,
23210 ) -> (
23211 Rc<RefCell<Pool>>,
23212 H2Shell<PacedSocket>,
23213 Context<TestListener>,
23214 Router,
23215 std::net::TcpStream,
23216 ) {
23217 let (pool, mut connection, context, router, peer) = two_requests_read(usize::MAX);
23218 connection.core.flood_detector = h2_flood_detector::H2FloodDetector::new(
23219 H2FloodConfig::from_optional(
23220 None,
23221 None,
23222 None,
23223 None,
23224 None,
23225 None,
23226 glitch_limit,
23227 None,
23228 Some(10),
23229 None,
23230 None,
23231 None,
23232 None,
23233 None,
23234 ),
23235 connection.core.now,
23236 );
23237 connection.core.settings_sent_at = None;
23238 for _ in 0..pre_response_resets {
23239 assert!(
23240 connection
23241 .core
23242 .flood_detector
23243 .record_rst_received(false, false, connection.core.now)
23244 .is_none(),
23245 "premise: the resets on record stay under the cap"
23246 );
23247 }
23248 (pool, connection, context, router, peer)
23249 }
23250
23251 /// A complete `GET /` HEADERS frame on `stream_id`, from the static
23252 /// table and literals without indexing, so it decodes whatever the
23253 /// peer's dynamic table holds.
23254 fn static_get_headers(stream_id: StreamId) -> Vec<u8> {
23255 let mut block = vec![0x82, 0x84, 0x87, 0x01, 11];
23256 block.extend_from_slice(b"example.com");
23257 orphan_frame(
23258 1,
23259 parser::FLAG_END_STREAM | parser::FLAG_END_HEADERS,
23260 stream_id,
23261 block.len() as u32,
23262 &block,
23263 )
23264 }
23265
23266 /// Feed `inbound` to `connection`, flush what it answers, and return
23267 /// every frame on the wire so far.
23268 fn frames_after(
23269 connection: &mut H2Shell<PacedSocket>,
23270 context: &mut Context<TestListener>,
23271 router: &mut Router,
23272 inbound: &[u8],
23273 ) -> Vec<PeerFrame> {
23274 connection.socket.inbound.extend(inbound);
23275 // A pass may stop after a frame that queued output, so drive more
23276 // passes than there are frames.
23277 for _ in 0..256 {
23278 connection.core.readiness.event.insert(Ready::READABLE);
23279 if connection.core.readiness.filter_interest().is_readable() {
23280 connection.readable(context, EndpointClient(router));
23281 }
23282 connection.core.readiness.event.insert(Ready::WRITABLE);
23283 if connection.core.readiness.filter_interest().is_writable() {
23284 connection.writable(context, EndpointClient(router));
23285 }
23286 }
23287 peer_frames(&connection.socket.wire).expect("whole frames on the wire")
23288 }
23289
23290 /// The error code of the first frame of type `kind` (3 RST_STREAM, 7
23291 /// GOAWAY) on `stream_id` (0 for GOAWAY) in `frames`.
23292 fn error_code_of(frames: &[PeerFrame], kind: u8, stream_id: StreamId) -> Option<u32> {
23293 let at = if kind == 7 { 4 } else { 0 };
23294 frames
23295 .iter()
23296 .find(|(k, _, sid, _)| *k == kind && *sid == stream_id)
23297 .map(|(_, _, _, payload)| {
23298 u32::from_be_bytes(payload[at..at + 4].try_into().expect("an error code"))
23299 })
23300 }
23301
23302 /// RFC 9113 §5.1.1: a new stream's id must be greater than every id the
23303 /// client already used, refused ones included. After stream 65 is refused
23304 /// under flood pressure, HEADERS on 55 opens no stream: it is a
23305 /// connection error of type PROTOCOL_ERROR, and the refused run stays
23306 /// `(65, 65)`.
23307 ///
23308 /// TO SEE THIS RED: in `ConnectionH2::handle_header_state`, compare a new
23309 /// client stream's id with `last_stream_id` only, as before the fix. Stream
23310 /// 55 is then taken for a new stream and refused too, and the debug build
23311 /// panics with `the refused run ends at the stream just refused`.
23312 #[test]
23313 fn headers_below_a_refused_stream_id_is_a_protocol_error() {
23314 let (_pool, mut connection, mut context, mut router, _peer) =
23315 soft_refusal_connection(None, 6);
23316 let mut wire = static_get_headers(65);
23317 wire.extend(static_get_headers(55));
23318 let frames = frames_after(&mut connection, &mut context, &mut router, &wire);
23319 assert_eq!(
23320 error_code_of(&frames, 3, 65),
23321 Some(H2Error::RefusedStream as u32),
23322 "premise: stream 65 is refused under flood pressure, got {frames:?}"
23323 );
23324 assert_eq!(
23325 error_code_of(&frames, 7, 0),
23326 Some(H2Error::ProtocolError as u32),
23327 "HEADERS on an id below a refused one must end the connection with \
23328 GOAWAY(PROTOCOL_ERROR), got {frames:?}"
23329 );
23330 assert_eq!(
23331 error_code_of(&frames, 3, 55),
23332 None,
23333 "stream 55 must not be refused as a new stream"
23334 );
23335 assert_eq!(connection.core.flood_refused_streams, Some((65, 65)));
23336 }
23337
23338 /// Soft refusals count toward the glitch budget once the client
23339 /// acknowledged Sōzu's SETTINGS, like the draining and
23340 /// SETTINGS_MAX_CONCURRENT_STREAMS refusals: a client that keeps opening
23341 /// streams it is refused, without resetting any, ends with
23342 /// GOAWAY(ENHANCE_YOUR_CALM).
23343 ///
23344 /// TO SEE THIS RED: in `ConnectionH2::handle_header_state`, drop the
23345 /// `record_glitch` call of the soft-refusal branch. Every stream is then
23346 /// refused and no GOAWAY ever comes.
23347 #[test]
23348 fn soft_refusals_count_toward_the_glitch_budget() {
23349 const GLITCH_LIMIT: u32 = 20;
23350 let (_pool, mut connection, mut context, mut router, _peer) =
23351 soft_refusal_connection(Some(GLITCH_LIMIT), 6);
23352 let mut wire = Vec::new();
23353 for n in 0..2 * GLITCH_LIMIT {
23354 wire.extend(static_get_headers(5 + 2 * n));
23355 }
23356 let frames = frames_after(&mut connection, &mut context, &mut router, &wire);
23357 assert_eq!(
23358 error_code_of(&frames, 3, 5),
23359 Some(H2Error::RefusedStream as u32),
23360 "premise: the first new stream is refused under flood pressure"
23361 );
23362 assert_eq!(
23363 error_code_of(&frames, 7, 0),
23364 Some(H2Error::EnhanceYourCalm as u32),
23365 "a client that keeps opening refused streams must exhaust the glitch \
23366 budget, got {} RST_STREAM frames and no GOAWAY",
23367 frames.iter().filter(|(kind, ..)| *kind == 3).count()
23368 );
23369 assert!(
23370 frames.iter().filter(|(kind, ..)| *kind == 3).count() <= GLITCH_LIMIT as usize + 1,
23371 "the glitch budget bounds the refusals"
23372 );
23373 }
23374
23375 /// A RST_STREAM the client sends on the wire for a stream refused under
23376 /// flood pressure goes through the closed-stream branch of
23377 /// `ConnectionH2::handle_header_state`, which counts one glitch, and then
23378 /// reaches `ConnectionH2::handle_rst_stream_frame`: refusals stop for the
23379 /// connection, so its next stream is opened, and the reset counts as a
23380 /// pre-response one, so with ten pre-response resets on record it
23381 /// trips the cap's floor of 10.
23382 ///
23383 /// TO SEE THIS RED: in `ConnectionH2::handle_rst_stream_frame`, delete
23384 /// the `flood_refused_streams` arm: `a client that resets a refused
23385 /// stream gets no more refusals` fails, stream 7 is refused, and the
23386 /// reset counts as response-started, so no GOAWAY comes.
23387 #[test]
23388 fn a_client_reset_of_a_refused_stream_on_the_wire_ends_the_refusals() {
23389 let rst = |stream_id: StreamId| {
23390 orphan_frame(3, 0, stream_id, 4, &(H2Error::Cancel as u32).to_be_bytes())
23391 };
23392
23393 let (_pool, mut connection, mut context, mut router, _peer) =
23394 soft_refusal_connection(None, 6);
23395 let frames = frames_after(
23396 &mut connection,
23397 &mut context,
23398 &mut router,
23399 &static_get_headers(5),
23400 );
23401 assert_eq!(
23402 error_code_of(&frames, 3, 5),
23403 Some(H2Error::RefusedStream as u32),
23404 "premise: stream 5 is refused under flood pressure"
23405 );
23406 let glitches = connection.core.flood_detector.glitch_count();
23407 frames_after(&mut connection, &mut context, &mut router, &rst(5));
23408 assert_eq!(
23409 connection.core.flood_detector.glitch_count(),
23410 glitches + 1,
23411 "the reset of a refused stream costs one glitch"
23412 );
23413 assert!(
23414 connection.core.flood_refusal_ignored,
23415 "a client that resets a refused stream gets no more refusals"
23416 );
23417 let frames = frames_after(
23418 &mut connection,
23419 &mut context,
23420 &mut router,
23421 &static_get_headers(7),
23422 );
23423 assert_eq!(
23424 error_code_of(&frames, 3, 7),
23425 None,
23426 "no stream is refused after the client reset a refused one"
23427 );
23428 assert!(
23429 connection.core.stream_table.get(7).is_some(),
23430 "stream 7 is opened"
23431 );
23432
23433 // Ten pre-response resets on record: the reset of a refused stream is
23434 // the eleventh, past the floor.
23435 let (_pool, mut connection, mut context, mut router, _peer) =
23436 soft_refusal_connection(None, 10);
23437 let mut wire = static_get_headers(5);
23438 wire.extend(rst(5));
23439 let frames = frames_after(&mut connection, &mut context, &mut router, &wire);
23440 assert_eq!(
23441 error_code_of(&frames, 3, 5),
23442 Some(H2Error::RefusedStream as u32),
23443 "premise: stream 5 is refused under flood pressure"
23444 );
23445 assert_eq!(
23446 error_code_of(&frames, 7, 0),
23447 Some(H2Error::EnhanceYourCalm as u32),
23448 "the reset of a refused stream must count as pre-response, got {frames:?}"
23449 );
23450 }
23451
23452 /// HEADERS without END_STREAM, `len` bytes of DATA split in frames of at
23453 /// most 16 KiB, and a trailer section with END_STREAM on `stream_id`:
23454 /// what a client streaming an upload with request trailers sends before
23455 /// it reads anything back.
23456 fn upload_with_trailers(stream_id: StreamId, len: usize) -> Vec<u8> {
23457 let mut block = vec![0x83, 0x84, 0x87, 0x01, 11];
23458 block.extend_from_slice(b"example.com");
23459 let mut wire = orphan_frame(
23460 1,
23461 parser::FLAG_END_HEADERS,
23462 stream_id,
23463 block.len() as u32,
23464 &block,
23465 );
23466 wire.extend(refused_stream_data(stream_id, len, 0));
23467 // `x-t: 1`, a literal with incremental indexing and a new name: it
23468 // becomes the newest dynamic table entry, index 62 (RFC 7541 §6.2.1).
23469 let trailers = [0x40, 3, b'x', b'-', b't', 1, b'1'];
23470 wire.extend(orphan_frame(
23471 1,
23472 parser::FLAG_END_HEADERS | parser::FLAG_END_STREAM,
23473 stream_id,
23474 trailers.len() as u32,
23475 &trailers,
23476 ));
23477 wire
23478 }
23479
23480 /// `len` bytes of DATA on `stream_id` in frames of at most 16 KiB, the
23481 /// last one carrying `last_flags`.
23482 fn refused_stream_data(stream_id: StreamId, len: usize, last_flags: u8) -> Vec<u8> {
23483 let mut wire = Vec::new();
23484 let mut left = len;
23485 while left > 0 {
23486 let size = left.min(16_384);
23487 left -= size;
23488 let flags = if left == 0 { last_flags } else { 0 };
23489 wire.extend(orphan_frame(
23490 0,
23491 flags,
23492 stream_id,
23493 size as u32,
23494 &vec![b'x'; size],
23495 ));
23496 }
23497 wire
23498 }
23499
23500 /// RFC 9113 §5.1: the frames a client sent on a stream before it read
23501 /// Sōzu's `REFUSED_STREAM` are ignored, like those on a stream Sōzu
23502 /// reset. A refused upload carrying request trailers costs the client
23503 /// that stream only: no `RST_STREAM(STREAM_CLOSED)` for its DATA, no
23504 /// GOAWAY for its trailers, and the streams already open, and the
23505 /// connection, carry on.
23506 ///
23507 /// TO SEE THIS RED: in `ConnectionH2::handle_header_state`, drop the
23508 /// `remember_refused` call of the soft-refusal branch. The DATA
23509 /// frame then takes the closed-stream branch, and the trailers end the
23510 /// connection with GOAWAY(PROTOCOL_ERROR).
23511 #[test]
23512 fn trailers_on_a_refused_stream_are_ignored() {
23513 let (_pool, mut connection, mut context, mut router, _peer) =
23514 soft_refusal_connection(None, 6);
23515 let glitches = connection.core.flood_detector.glitch_count();
23516 let frames = frames_after(
23517 &mut connection,
23518 &mut context,
23519 &mut router,
23520 &upload_with_trailers(5, 1000),
23521 );
23522 assert_eq!(
23523 error_code_of(&frames, 3, 5),
23524 Some(H2Error::RefusedStream as u32),
23525 "premise: stream 5 is refused under flood pressure, got {frames:?}"
23526 );
23527 assert_eq!(
23528 frames
23529 .iter()
23530 .filter(|(kind, _, sid, _)| *kind == 3 && *sid == 5)
23531 .count(),
23532 1,
23533 "the refused stream gets its REFUSED_STREAM and nothing more, got {frames:?}"
23534 );
23535 assert_eq!(
23536 error_code_of(&frames, 7, 0),
23537 None,
23538 "frames on a refused stream must not end the connection, got {frames:?}"
23539 );
23540 assert_eq!(
23541 connection.core.flood_detector.glitch_count(),
23542 glitches + 1,
23543 "only the refusal itself costs a glitch"
23544 );
23545 assert!(
23546 connection.core.stream_table.get(1).is_some()
23547 && connection.core.stream_table.get(3).is_some(),
23548 "the streams already open survive"
23549 );
23550 assert!(
23551 !matches!(connection.core.state, H2State::GoAway | H2State::Error),
23552 "the connection survives, got {:?}",
23553 connection.core.state
23554 );
23555 // The ignored trailer block was still decoded (RFC 9113 §4.3): the
23556 // entry it indexed is the newest in the connection's dynamic table.
23557 let mut decoded = Vec::new();
23558 let status = connection
23559 .core
23560 .hpack
23561 .decoder_mut()
23562 .decode_with_cb(&[0x80 | 62], |k, v| {
23563 decoded.push((k.into_owned(), v.into_owned()));
23564 });
23565 assert!(status.is_ok(), "index 62 must resolve, got {status:?}");
23566 assert_eq!(
23567 decoded,
23568 vec![(b"x-t".to_vec(), b"1".to_vec())],
23569 "the ignored trailers must keep the HPACK decoder in step with the peer"
23570 );
23571 // The connection reads on: a later PING is answered.
23572 let frames = frames_after(&mut connection, &mut context, &mut router, &orphan_ping());
23573 assert!(
23574 frames.iter().any(|(kind, flags, _, payload)| *kind == 6
23575 && *flags & 1 == 1
23576 && payload == b"pingpong"),
23577 "the connection must keep reading after the ignored frames, got {frames:?}"
23578 );
23579 }
23580
23581 /// RFC 9113 §5.1 and §6.9: DATA a client sent on a refused stream within
23582 /// the stream's initial window — a browser upload filling it in 16 KiB
23583 /// frames — is ignored without a glitch or a `RST_STREAM(STREAM_CLOSED)`
23584 /// per frame, and still counts toward the connection's receive window,
23585 /// which Sōzu credits back.
23586 ///
23587 /// TO SEE THIS RED: in `ConnectionH2::handle_header_state`, drop the
23588 /// `remember_refused` call of the soft-refusal branch. Each of
23589 /// the four DATA frames then costs a glitch and a RST_STREAM.
23590 #[test]
23591 fn data_on_a_refused_stream_is_ignored_and_credited() {
23592 let (_pool, mut connection, mut context, mut router, _peer) =
23593 soft_refusal_connection(None, 6);
23594 // Half of this is the threshold at which a stream-0 WINDOW_UPDATE
23595 // credits the received bytes back.
23596 connection.core.connection_config.initial_connection_window = 65_535;
23597 let mut block = vec![0x83, 0x84, 0x87, 0x01, 11];
23598 block.extend_from_slice(b"example.com");
23599 let mut wire = orphan_frame(1, parser::FLAG_END_HEADERS, 5, block.len() as u32, &block);
23600 let window = connection.core.local_settings.settings_initial_window_size as usize;
23601 wire.extend(refused_stream_data(5, window, parser::FLAG_END_STREAM));
23602 let glitches = connection.core.flood_detector.glitch_count();
23603 let frames = frames_after(&mut connection, &mut context, &mut router, &wire);
23604 assert_eq!(
23605 error_code_of(&frames, 3, 5),
23606 Some(H2Error::RefusedStream as u32),
23607 "premise: stream 5 is refused under flood pressure, got {frames:?}"
23608 );
23609 assert_eq!(
23610 frames
23611 .iter()
23612 .filter(|(kind, _, sid, _)| *kind == 3 && *sid == 5)
23613 .count(),
23614 1,
23615 "DATA on a refused stream must not be answered with RST_STREAM, got {frames:?}"
23616 );
23617 assert_eq!(
23618 connection.core.flood_detector.glitch_count(),
23619 glitches + 1,
23620 "DATA within the stream's window on a refused stream costs no glitch"
23621 );
23622 let credited: u32 = frames
23623 .iter()
23624 .filter(|(kind, _, sid, _)| *kind == 8 && *sid == 0)
23625 .map(|(_, _, _, payload)| {
23626 u32::from_be_bytes(payload[..4].try_into().expect("an increment")) & 0x7fff_ffff
23627 })
23628 .sum();
23629 assert_eq!(
23630 credited as usize, window,
23631 "the ignored DATA must be credited to the connection window exactly once"
23632 );
23633 assert_eq!(
23634 error_code_of(&frames, 7, 0),
23635 None,
23636 "no GOAWAY, got {frames:?}"
23637 );
23638 }
23639
23640 /// A drain refuses new streams; a client that sent a whole upload with
23641 /// request trailers on one of them before it read the GOAWAY sees its
23642 /// stream refused, and the drain still ends with GOAWAY(NO_ERROR).
23643 ///
23644 /// TO SEE THIS RED: in `ConnectionH2::refuse_stream_and_discard`, drop
23645 /// the `remember_refused` call. The trailers then end the
23646 /// connection with GOAWAY(PROTOCOL_ERROR) before the drain completes.
23647 #[test]
23648 fn a_drain_with_a_refused_upload_carrying_trailers_ends_with_no_error() {
23649 let (_pool, mut connection, mut context, mut router, _peer) = two_requests_read(usize::MAX);
23650 connection.core.drain.__test_set_draining();
23651 let frames = frames_after(
23652 &mut connection,
23653 &mut context,
23654 &mut router,
23655 &upload_with_trailers(5, 1000),
23656 );
23657 assert_eq!(
23658 error_code_of(&frames, 3, 5),
23659 Some(H2Error::RefusedStream as u32),
23660 "premise: the drain refuses stream 5, got {frames:?}"
23661 );
23662 let now = connection.core.now;
23663 let _ = connection.core.graceful_goaway(now);
23664 let frames = frames_after(&mut connection, &mut context, &mut router, &[]);
23665 assert_eq!(
23666 error_code_of(&frames, 7, 0),
23667 Some(H2Error::NoError as u32),
23668 "the drain must end with GOAWAY(NO_ERROR), got {frames:?}"
23669 );
23670 }
23671
23672 /// A mix of cancels: one stream in ten reset before its response,
23673 /// seventeen in twenty after it, the rest completed. The pre-response
23674 /// resets are the pre-response cap's business; the received-RST lifetime
23675 /// cap compares only the other resets with the answered streams, and must
23676 /// not trip.
23677 ///
23678 /// TO SEE THIS RED: in `H2FloodDetector::record_rst_lifetime`, compare
23679 /// `total_rst_received_lifetime` instead of the resets outside the
23680 /// pre-response count with `streams_opened`. The pre-response resets then
23681 /// push the count past the answered streams and the cap sends GOAWAY.
23682 #[test]
23683 fn pre_response_resets_do_not_count_toward_the_lifetime_cap() {
23684 let (_pool, mut connection, _peer, mut context, mut router) = frontend_rst_fixture(4);
23685 let mut stream_id = 1;
23686 for block in 0..5 {
23687 for slot in 0..20 {
23688 if slot == 19 {
23689 // A stream that completed: counted where the recycle path
23690 // counts it (`try_recycle_server_stream`).
23691 connection.core.flood_detector.record_stream_opened();
23692 continue;
23693 }
23694 let response_started = slot >= 2;
23695 open_routed_frontend_stream(
23696 &mut connection,
23697 &mut context,
23698 stream_id,
23699 response_started,
23700 );
23701 assert!(
23702 !client_cancels(&mut connection, &mut context, &mut router, stream_id),
23703 "block {block}, slot {slot}: a 10 % pre-response / 85 % \
23704 post-response / 5 % clean mix must not send GOAWAY"
23705 );
23706 stream_id += 2;
23707 }
23708 }
23709 }
23710
23711 /// The received-RST lifetime cap still bounds resets of streams that are
23712 /// already gone: here every answered stream is reset twice, so these
23713 /// resets outnumber the answered streams as soon as the floor is passed.
23714 /// Guard against neutering the cap.
23715 #[test]
23716 fn resets_of_streams_already_gone_trip_the_lifetime_cap() {
23717 let (_pool, mut connection, _peer, mut context, mut router) = frontend_rst_fixture(4);
23718 let mut tripped_after = None;
23719 for n in 0..16u32 {
23720 let stream_id = 2 * n + 1;
23721 open_routed_frontend_stream(&mut connection, &mut context, stream_id, true);
23722 if client_cancels(&mut connection, &mut context, &mut router, stream_id)
23723 || client_cancels(&mut connection, &mut context, &mut router, stream_id)
23724 {
23725 tripped_after = Some(n + 1);
23726 break;
23727 }
23728 }
23729 assert_eq!(
23730 tripped_after,
23731 Some(3),
23732 "the reset that takes the count past the floor of 4 while the \
23733 resets outnumber the answered streams must send GOAWAY"
23734 );
23735 }
23736
23737 /// A stream Sōzu answers 502/503/504 without selecting a backend — no
23738 /// backend in the cluster, the session or buffer limit hit before a
23739 /// backend was picked, a routing error — never cost a backend anything,
23740 /// and must not count toward the RST caps' denominator.
23741 ///
23742 /// TO SEE THIS RED: add `|| matches!(stream.context.status,
23743 /// Some(502..=504))` back to `routed_to_a_backend`.
23744 #[test]
23745 fn a_5xx_answered_without_a_backend_is_not_routed() {
23746 let pool = make_pool_for_invariant_16();
23747 let (mut connection, _peer) = test_h2_connection(&pool, None);
23748 let mut context = test_context(&pool);
23749 let answers = context.listener.borrow().get_answers().clone();
23750 for code in [502, 503, 504] {
23751 let gid = context
23752 .create_stream(Ulid::generate(), 65_535)
23753 .expect("the test pool must hand out stream buffers");
23754 crate::protocol::mux::answers::set_default_answer(
23755 &mut context.streams[gid],
23756 &mut connection.core.readiness,
23757 code,
23758 &answers.borrow(),
23759 );
23760 let stream = &context.streams[gid];
23761 assert_eq!(stream.context.status, Some(code), "premise");
23762 assert!(
23763 !routed_to_a_backend(stream),
23764 "a {code} answered before any backend was selected must not count"
23765 );
23766 context.streams[gid].state = StreamState::Recycle;
23767 }
23768 }
23769
23770 /// A stream answered 502/503/504 after a backend was selected for it —
23771 /// the backend refused the connection, timed out, or reset — keeps
23772 /// counting, so a backend outage does not turn the client's ordinary
23773 /// cancels into a Rapid Reset verdict.
23774 ///
23775 /// TO SEE THIS RED: drop `|| stream.context.backend_id.is_some()` from
23776 /// `routed_to_a_backend`.
23777 ///
23778 /// Past the first failures the outage no longer selects a backend at all:
23779 /// every backend is backing off and the 503 is answered with
23780 /// `backends_unavailable` set, which counts the same way.
23781 ///
23782 /// TO SEE THIS RED for the second half: drop
23783 /// `|| stream.context.backends_unavailable` from `routed_to_a_backend`;
23784 /// `test_h2_cancels_during_a_backend_outage_keep_the_connection` then
23785 /// fails end to end too.
23786 #[test]
23787 fn a_5xx_answered_during_a_backend_outage_is_routed() {
23788 let pool = make_pool_for_invariant_16();
23789 let (mut connection, _peer) = test_h2_connection(&pool, None);
23790 let mut context = test_context(&pool);
23791 let answers = context.listener.borrow().get_answers().clone();
23792 for code in [502, 503, 504] {
23793 let gid = context
23794 .create_stream(Ulid::generate(), 65_535)
23795 .expect("the test pool must hand out stream buffers");
23796 context.streams[gid].context.backend_id = Some(Rc::from("down-backend"));
23797 crate::protocol::mux::answers::set_default_answer(
23798 &mut context.streams[gid],
23799 &mut connection.core.readiness,
23800 code,
23801 &answers.borrow(),
23802 );
23803 let stream = &context.streams[gid];
23804 assert_eq!(stream.context.status, Some(code), "premise");
23805 assert!(
23806 routed_to_a_backend(stream),
23807 "a {code} answered for a selected backend must count"
23808 );
23809 context.streams[gid].state = StreamState::Recycle;
23810 }
23811 let gid = context
23812 .create_stream(Ulid::generate(), 65_535)
23813 .expect("the test pool must hand out stream buffers");
23814 context.streams[gid].context.backends_unavailable = true;
23815 crate::protocol::mux::answers::set_default_answer(
23816 &mut context.streams[gid],
23817 &mut connection.core.readiness,
23818 503,
23819 &answers.borrow(),
23820 );
23821 assert!(
23822 routed_to_a_backend(&context.streams[gid]),
23823 "a 503 for a cluster whose backends are all failing must count"
23824 );
23825 }
23826
23827 /// An inbound `GOAWAY` retires every stream above `last_stream_id`
23828 /// without `Connection::end_stream`, so it owes each of their releases.
23829 #[test]
23830 fn invariant_14_an_inbound_goaway_releases_every_retired_charge() {
23831 let pool = make_pool_for_invariant_16();
23832 let mut context = test_context(&pool);
23833 let mut fixture = ledger_fixture(&pool);
23834 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
23835
23836 let _gids = open_streams(&mut fixture, &mut context, 3);
23837 let Connection::H2(shell) = &mut fixture.connection else {
23838 unreachable!("the fixture was built as H2");
23839 };
23840 // `last_stream_id = 0` retires all three: the peer says it processed
23841 // none of them.
23842 shell.core.handle_goaway_frame(
23843 parser::GoAway {
23844 last_stream_id: 0,
23845 error_code: H2Error::NoError as u32,
23846 // Empty debug data: `Slice` is an offset/length pair into
23847 // a buffer this scenario does not carry.
23848 additional_debug_data: kawa::repr::Slice::new(b"", b""),
23849 },
23850 &mut context,
23851 EndpointClient(&mut router),
23852 );
23853
23854 assert_eq!(
23855 reconcile(&context.backend_deltas),
23856 (0, 0),
23857 "every retired stream must release the charge it took"
23858 );
23859 assert_eq!(
23860 settle(&mut fixture, &mut context),
23861 0,
23862 "a GOAWAY that retires every stream must leave no charge behind"
23863 );
23864 }
23865
23866 /// An H1 backend taken back out of the keep-alive pool must be charged
23867 /// for the request it is being reused for.
23868 ///
23869 /// This one records a BEHAVIOUR CHANGE, not just a refactor. Before
23870 /// #1340's Question 12 the charge was taken *before* the inner
23871 /// `start_stream`, under a `BackendStatus::Connected` guard — and an H1
23872 /// connection coming out of the pool is `BackendStatus::KeepAlive` at
23873 /// that moment, because `ConnectionH1::start_stream` is what flips it to
23874 /// `Connected`. So the reuse took no charge, while the matching
23875 /// `Connection::end_stream` released one (it runs before
23876 /// `ConnectionH1::end_stream` flips the status back, so its own
23877 /// `Connected` guard passes). Every keep-alive reuse cycle therefore net
23878 /// `-1`d the backend, `saturating_sub` floored it at zero, and a reused
23879 /// H1 connection reported no in-flight request while it served one —
23880 /// exactly the monotonic drift invariant 14 names, biasing least-loaded
23881 /// and PeakEWMA balancing toward backends that are already busy.
23882 ///
23883 /// Recording the charge after the inner call fixes it, because the flip
23884 /// has happened by then. Taking it afterwards is also what removes the
23885 /// rollback, so the two properties are one edit.
23886 #[test]
23887 fn invariant_14_an_h1_keep_alive_reuse_is_charged() {
23888 let pool = make_pool_for_invariant_16();
23889 let mut context = test_context(&pool);
23890 let (socket, _peer) = connected_socket();
23891 let address: std::net::SocketAddr =
23892 "127.0.0.1:2".parse().expect("backend address must parse");
23893 let backend = Rc::new(RefCell::new(crate::backends::Backend::new(
23894 "h1-keepalive-backend",
23895 address,
23896 None,
23897 None,
23898 None,
23899 )));
23900 let mut registry = crate::protocol::mux::BackendRegistry::default();
23901 let backend_id = registry.id_for(&backend);
23902 let session_ulid = Ulid::generate();
23903 let mut connection = Connection::new_h1_client(
23904 session_ulid,
23905 SessionTcpStream::new(socket, session_ulid, Some(address)),
23906 "h1-keepalive-cluster".into(),
23907 backend_id,
23908 Duration::from_secs(30),
23909 );
23910 // Park it in the pool, the state `Router::plan_connect`'s H1 reuse arm
23911 // picks a backend up from.
23912 if let Position::Client(_, _, status) = connection.position_mut() {
23913 *status = BackendStatus::KeepAlive;
23914 }
23915
23916 let gid = context
23917 .create_stream(Ulid::generate(), 65_535)
23918 .expect("the test pool must hand out stream buffers");
23919 assert!(
23920 connection.start_stream(gid, &mut context),
23921 "premise: a keep-alive H1 backend must accept the reused stream"
23922 );
23923 assert_eq!(
23924 reconcile(&context.backend_deltas),
23925 (1, 0),
23926 "a keep-alive reuse must charge the request it is being reused for"
23927 );
23928
23929 connection.end_stream(gid, &mut context);
23930 assert_eq!(
23931 reconcile(&context.backend_deltas),
23932 (0, 0),
23933 "and the charge must be released when that request ends"
23934 );
23935 let backends = RefCell::new(crate::backends::BackendMap::with_seed(0));
23936 for delta in std::mem::take(&mut context.backend_deltas) {
23937 registry.apply(delta, &backends);
23938 }
23939 assert_eq!(
23940 backend.borrow().active_requests,
23941 0,
23942 "a full reuse cycle must leave active_requests where it found it"
23943 );
23944 }
23945
23946 /// `cancel_timed_out_streams` reaps an idle stream on the same terms:
23947 /// removed outside `Connection::end_stream`, so it releases its own
23948 /// charge.
23949 #[test]
23950 fn invariant_14_a_timed_out_stream_releases_its_charge() {
23951 let pool = make_pool_for_invariant_16();
23952 let mut context = test_context(&pool);
23953 let mut fixture = ledger_fixture(&pool);
23954 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
23955
23956 let _gids = open_streams(&mut fixture, &mut context, 1);
23957 // Push the mux's clock far past the stream idle timeout the fixture
23958 // configured, so the reaper finds the stream expired.
23959 context.now += Duration::from_secs(120);
23960 let Connection::H2(shell) = &mut fixture.connection else {
23961 unreachable!("the fixture was built as H2");
23962 };
23963 let mut endpoint = EndpointClient(&mut router);
23964 shell
23965 .core
23966 .cancel_timed_out_streams(&mut context, &mut endpoint);
23967
23968 assert_eq!(
23969 reconcile(&context.backend_deltas),
23970 (0, 0),
23971 "a reaped stream must release the charge it took"
23972 );
23973 assert_eq!(
23974 settle(&mut fixture, &mut context),
23975 0,
23976 "reaping the only stream must leave no charge behind"
23977 );
23978 }
23979
23980 // ── `backend_header_time`, the H2 half (sozu-proxy/sozu#426) ─────────
23981 //
23982 // DO NOT READ THIS TEST AS COVERING H1. It drives
23983 // `ConnectionH2::handle_headers_frame` only. The H1 twin lives in
23984 // `ConnectionH1::readable` (`lib/src/protocol/mux/h1.rs`) and is pinned by
23985 // its own test there.
23986
23987 /// Stage a response field block in the connection's stream-0 storage and
23988 /// hand back the `Headers` frame that names it, the way
23989 /// `ConnectionH2::handle_read` hands one to
23990 /// `ConnectionH2::handle_headers_frame`: the fragment is a `(start, len)`
23991 /// window over `ConnectionH2::zero`'s buffer, not an owned slice.
23992 ///
23993 /// `0x88` is one HPACK indexed-header-field octet naming static-table
23994 /// entry 8, `:status: 200` (RFC 7541 Appendix A) — a complete, valid
23995 /// response field block in a single byte, so this needs no encoder.
23996 fn stage_response_headers(core: &mut ConnectionH2, stream_id: u32) -> parser::Headers {
23997 core.zero.storage.space()[..1].copy_from_slice(&[0x88]);
23998 core.zero.storage.fill(1);
23999 let buffer = core.zero.storage.buffer();
24000 parser::Headers {
24001 stream_id,
24002 priority: None,
24003 header_block_fragment: kawa::repr::Slice::new(buffer, &buffer[..1]),
24004 end_stream: false,
24005 end_headers: true,
24006 }
24007 }
24008
24009 /// The H2 twin of
24010 /// `the_h1_backend_header_instant_lands_on_the_headers_not_the_response_end`:
24011 /// a backend response HEADERS frame arms the time-to-first-header-byte
24012 /// instant, and it arms it strictly before the response ends.
24013 ///
24014 /// `end_stream: false` is what makes it discriminating — the response body
24015 /// is still outstanding, so `backend_stop` (which every one of
24016 /// `ConnectionH2`'s four sites owns) must still be absent while the header
24017 /// instant already exists. An instant placed at any of those sites cannot
24018 /// satisfy that.
24019 ///
24020 /// The ordering is asserted on `Duration`s rather than on the millisecond
24021 /// values the histogram stores, for the same reason as its H1 twin: at
24022 /// millisecond resolution a fast test rounds both to 0 and the relation
24023 /// holds however wrong the placement is.
24024 ///
24025 /// To SEE THIS RED: move `stream.metrics.backend_headers_received();` out
24026 /// of the `was_initial && self.position.is_client()` branch of
24027 /// `ConnectionH2::handle_headers_frame` and put it immediately after the
24028 /// `stream.metrics.backend_stop();` that opens
24029 /// `ConnectionH2::complete_server_stream` — the nominal `H2::Complete`
24030 /// response-end site. The frame under test carries no END_STREAM, so
24031 /// nothing arms the instant and the test reports no header time at all.
24032 #[test]
24033 fn the_h2_backend_header_instant_lands_on_the_headers_not_the_response_end() {
24034 let pool = make_pool_for_invariant_16();
24035 let mut context = test_context(&pool);
24036 let mut fixture = ledger_fixture(&pool);
24037 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
24038
24039 let gids = open_streams(&mut fixture, &mut context, 1);
24040 let gid = gids[0];
24041 let metrics = &mut context.streams[gid].metrics;
24042 metrics.backend_id = Some("ledger-backend".into());
24043 metrics.backend_start();
24044 metrics.backend_connected();
24045
24046 // Premise: nothing has been received, so neither instant can be stale.
24047 assert_eq!(context.streams[gid].metrics.backend_header_time(), None);
24048 assert_eq!(context.streams[gid].metrics.backend_stop, None);
24049
24050 let Connection::H2(shell) = &mut fixture.connection else {
24051 unreachable!("the fixture was built as H2");
24052 };
24053 let stream_id = *shell
24054 .core
24055 .stream_table
24056 .streams()
24057 .keys()
24058 .next()
24059 .expect("the scenario opened one stream");
24060 assert!(
24061 context.streams[gid].back.is_initial(),
24062 "premise: the backend read side must still be in its initial \
24063 parsing phase, or `was_initial` would not fire"
24064 );
24065
24066 let headers = stage_response_headers(&mut shell.core, stream_id);
24067 shell
24068 .core
24069 .handle_headers_frame(headers, &mut context, EndpointClient(&mut router));
24070
24071 let after_headers = context.streams[gid]
24072 .metrics
24073 .backend_headers_received
24074 .expect("a backend response HEADERS frame must arm the backend header instant");
24075 assert_eq!(
24076 context.streams[gid].metrics.backend_stop, None,
24077 "the frame carried no END_STREAM, so the response-end marker must \
24078 still be absent"
24079 );
24080
24081 // What `ConnectionH2::mark_end_of_stream` does once the response ends.
24082 context.streams[gid].metrics.backend_stop();
24083
24084 let metrics = &context.streams[gid].metrics;
24085 assert_eq!(
24086 metrics.backend_headers_received,
24087 Some(after_headers),
24088 "the response end must not move the header instant"
24089 );
24090 let header_time = metrics
24091 .backend_header_time()
24092 .expect("a completed H2 response must report a backend header time");
24093 let response_time = metrics
24094 .backend_response_time()
24095 .expect("a completed H2 response must report a backend response time");
24096 assert!(
24097 header_time <= response_time,
24098 "the first response-header byte arrives before the last response \
24099 byte: header_time {header_time:?} must not exceed response_time \
24100 {response_time:?}"
24101 );
24102 }
24103
24104 /// The server preface — SETTINGS, the connection-window enlargement and
24105 /// the ACK of the client's SETTINGS — leaves in ONE socket write.
24106 ///
24107 /// On `FrontRustls` every non-empty `socket_write` ends in its own
24108 /// `write_tls`, i.e. its own `writev(2)`. The enlargement used to be
24109 /// queued by `dispatch_writable_state`'s `(ServerSettings, Server)` arm,
24110 /// which runs after `drive_control_flush` already drained `zero`, so it
24111 /// cost a second write pass and a second syscall on every connection.
24112 ///
24113 /// TO SEE THIS RED: move the `gen_window_update` call out of the
24114 /// `(ClientSettings, Server)` readable arm back into a
24115 /// `queue_window_update(0, increment)` in the `(ServerSettings, Server)`
24116 /// writable arm. The write count becomes 2 and the first write carries 66
24117 /// bytes, not 79.
24118 #[test]
24119 fn server_preface_settings_window_update_and_ack_share_one_write() {
24120 use std::io::Read as _;
24121
24122 let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 8, 16384)));
24123 let (mut connection, mut peer) =
24124 connection_with_backpressure(&pool, 0, 0, H2State::ClientPreface);
24125 connection
24126 .core
24127 .readiness
24128 .interest
24129 .insert(Ready::READABLE | Ready::WRITABLE);
24130 let mut context = test_context(&pool);
24131 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
24132
24133 // Client preface + SETTINGS(MAX_CONCURRENT_STREAMS = 100).
24134 let mut preface = serializer::H2_PRI.as_bytes().to_vec();
24135 preface.extend_from_slice(&[0, 0, 6, 4, 0, 0, 0, 0, 0]);
24136 preface.extend_from_slice(&parser::SETTINGS_MAX_CONCURRENT_STREAMS.to_be_bytes());
24137 preface.extend_from_slice(&100u32.to_be_bytes());
24138 let now = connection.core.now;
24139 liveness_read(
24140 &mut connection,
24141 &mut context,
24142 &mut router,
24143 &mut peer,
24144 now,
24145 &preface,
24146 );
24147 liveness_write(&mut connection, &mut context, &mut router);
24148
24149 assert!(
24150 matches!(connection.core.state, H2State::Header),
24151 "premise: the handshake must have reached the proxying state, got {:?}",
24152 connection.core.state
24153 );
24154 assert_eq!(
24155 connection.socket.writes, 1,
24156 "SETTINGS, WINDOW_UPDATE(0) and the SETTINGS ACK must leave in a \
24157 single socket write"
24158 );
24159
24160 let settings_len = parser::FRAME_HEADER_SIZE
24161 + (parser::SETTINGS_ENTRY_SIZE * parser::SETTINGS_COUNT) as usize;
24162 let expected = settings_len + 13 + serializer::SETTINGS_ACKNOWLEDGEMENT.len();
24163 let mut wire = vec![0u8; expected];
24164 peer.set_read_timeout(Some(Duration::from_secs(5)))
24165 .expect("loopback read timeout must be settable");
24166 peer.read_exact(&mut wire)
24167 .expect("the peer must receive the whole server preface");
24168 assert_eq!(
24169 wire[3], 4,
24170 "the first frame must be SETTINGS (RFC 9113 §3.4)"
24171 );
24172 assert_eq!(wire[4], 0, "the first SETTINGS must not be an ACK");
24173 let window_update = &wire[settings_len..settings_len + 13];
24174 assert_eq!(
24175 window_update[3], 8,
24176 "the second frame must be WINDOW_UPDATE"
24177 );
24178 assert_eq!(&window_update[5..9], &[0, 0, 0, 0], "on the connection");
24179 assert_eq!(
24180 u32::from_be_bytes(window_update[9..13].try_into().unwrap()),
24181 H2ConnectionConfig::default().initial_connection_window - DEFAULT_INITIAL_WINDOW_SIZE,
24182 "the enlargement must grant exactly the configured surplus"
24183 );
24184 assert_eq!(
24185 &wire[settings_len + 13..],
24186 &serializer::SETTINGS_ACKNOWLEDGEMENT,
24187 "the ACK of the client's SETTINGS must close the preface"
24188 );
24189 }
24190
24191 /// A backend that sends SETTINGS again, without granting any connection
24192 /// credit, must not get the one-shot connection-window enlargement again:
24193 /// each repeat would add `initial_connection_window - 65535` to its send
24194 /// window until it passed 2^31-1 (RFC 9113 §6.9.1).
24195 ///
24196 /// TO SEE THIS RED: in `ConnectionH2::handle_settings_frame`, gate the
24197 /// enlargement on `self.flow_control.window() <= DEFAULT_INITIAL_WINDOW_SIZE`
24198 /// (our send window) instead of `connection_window_enlarged`. The three
24199 /// SETTINGS then queue three grants, coalesced into one stream-0 frame
24200 /// of three times the surplus. Verified 2026-10-01 against `3d3f077b`.
24201 #[test]
24202 fn repeated_backend_settings_enlarge_the_connection_window_once() {
24203 let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
24204 let (mut connection, _peer) = test_h2_connection(&pool, None);
24205 let mut context = test_context(&pool);
24206 connection.core.position = Position::Client(
24207 "settings-cluster".into(),
24208 super::super::BackendId {
24209 slot: super::super::BackendSlot(0),
24210 backend_id: Rc::from("settings-backend"),
24211 address: "127.0.0.1:1".parse().expect("a literal socket address"),
24212 metric_incarnation: Default::default(),
24213 },
24214 BackendStatus::Connected,
24215 );
24216 for _ in 0..3 {
24217 let result = connection.core.handle_settings_frame(
24218 parser::Settings {
24219 settings: vec![],
24220 ack: false,
24221 },
24222 &mut context,
24223 );
24224 assert!(
24225 matches!(result, MuxResult::Continue),
24226 "an empty SETTINGS must be accepted"
24227 );
24228 }
24229
24230 let mut buf = vec![0u8; 13 * 8];
24231 let (written, frames) = connection
24232 .core
24233 .flow_control
24234 .drain_window_updates_into(&mut buf);
24235 let grants: Vec<(u32, u32)> = buf[..written]
24236 .as_chunks::<13>()
24237 .0
24238 .iter()
24239 .map(|frame| {
24240 (
24241 u32::from_be_bytes(frame[5..9].try_into().unwrap()),
24242 u32::from_be_bytes(frame[9..13].try_into().unwrap()),
24243 )
24244 })
24245 .collect();
24246 assert_eq!(frames, grants.len());
24247 assert_eq!(
24248 grants,
24249 vec![(
24250 0,
24251 H2ConnectionConfig::default().initial_connection_window
24252 - DEFAULT_INITIAL_WINDOW_SIZE
24253 )],
24254 "three SETTINGS must enlarge the connection window exactly once"
24255 );
24256 }
24257}