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 // LIFECYCLE §9 invariant 9, write side. Outbound APPLICATION data
4145 // is activity; an acknowledgement is not. Both halves of this
4146 // condition are load-bearing and neither is implied by reaching
4147 // `H2Shell::write_streams`: an `H2StreamId::Other` id is what
4148 // makes these bytes a stream's rather than `self.zero`'s, and
4149 // `size > 0` is what makes them bytes the socket actually took.
4150 // `arm_timeout` reads `self.now`, the snapshot `writable()` adopted
4151 // at pass entry, so arming from here lands on the same instant the
4152 // top of the pass would have computed — the gate is the whole
4153 // change, not the moment.
4154 if committed > 0 {
4155 self.arm_timeout();
4156 }
4157 }
4158 if resuming {
4159 pass.resume_bytes = pass.resume_bytes.saturating_add(committed);
4160 } else {
4161 pass.stream_bytes = pass.stream_bytes.saturating_add(committed);
4162 }
4163 if let Some(amount) = cross_read_amount {
4164 // Resume path: same stream is parked waiting for buffer space.
4165 // Re-enable READABLE once the write freed enough room.
4166 let kawa = write_buffer(
4167 &mut self.zero,
4168 &mut context.streams,
4169 &self.position,
4170 stream_id,
4171 );
4172 if kawa.storage.available_space() >= amount {
4173 self.readiness.interest.insert(Ready::READABLE);
4174 }
4175 }
4176 pass.stalled = update_readiness_after_write(size, status, &mut self.readiness);
4177 }
4178
4179 /// After forwarding a 1xx informational response (100 Continue, 103 Early
4180 /// Hints), reset the back buffer and re-enable backend readable so the
4181 /// final response can arrive on the same stream. Returns true if the
4182 /// response was 1xx.
4183 fn handle_1xx_reset<E: Endpoint>(
4184 kawa: &mut GenericHttpStream,
4185 stream_state: StreamState,
4186 endpoint: &mut E,
4187 ) -> bool {
4188 let is_1xx = matches!(
4189 kawa.detached.status_line,
4190 kawa::StatusLine::Response { code, .. } if (100..200).contains(&code)
4191 );
4192 if !is_1xx {
4193 return false;
4194 }
4195 debug!(
4196 "{} H2 write_streams: 1xx informational forwarded, resetting back buffer",
4197 log_module_context!()
4198 );
4199 kawa.clear();
4200 if let StreamState::Linked(token) = stream_state {
4201 let readiness = endpoint.readiness_mut(token);
4202 readiness.interest.insert(Ready::READABLE);
4203 readiness.signal_pending_read();
4204 }
4205 true
4206 }
4207
4208 /// Re-arm edge-triggered WRITABLE event if rustls still has buffered TLS data.
4209 ///
4210 /// Takes the answer rather than asking for it: `tls_wants_write` is
4211 /// `H2Shell::tls_wants_write`'s result, read by the caller. That query was
4212 /// this composite's whole remaining socket reach, and handing it in is
4213 /// what leaves a body a byte-in / byte-out core can keep.
4214 ///
4215 /// When the caller reads it is not uniform, and cannot be. Five of the six
4216 /// call sites already hold the answer — four from the `h2_close` decision
4217 /// just taken on it, [`H2Shell::initiate_close_notify`] from its own branch
4218 /// condition — and pass that binding: nothing mutates the socket in
4219 /// between, so a second query would answer the same. The sixth, the
4220 /// [`H2ControlFlushTarget::Stalled`] arm of [`H2Shell::writable`], has no
4221 /// such binding and must read it AFTER the `H2Shell::flush_output_to_socket`
4222 /// whose stall brought it there: that write is what changes the answer.
4223 /// The stalled-drain tails of [`Self::flush_pending_control_frames`] used
4224 /// to read it and re-arm individually; its one output flush now answers
4225 /// [`H2ControlFlushTarget::Stalled`] and that arm re-arms.
4226 ///
4227 /// No re-arm follows a write the kernel refused
4228 /// (`kernel_refused_write`): the socket is full, the kernel raises
4229 /// the next WRITABLE edge once the peer reads, and that edge flushes the
4230 /// records. A synthetic edge would only repeat the refused write, and
4231 /// `Mux::ready_inner` did so on every inner iteration until
4232 /// `MAX_LOOP_ITERATIONS` counted `http.infinite_loop.error` whenever a
4233 /// TLS client stopped reading (sozu-proxy/sozu#1788). The close decisions
4234 /// still read `tls_wants_write` unchanged: a connection with records
4235 /// pending keeps WRITABLE interest and stays open until they are flushed.
4236 pub fn ensure_tls_flushed(&mut self, tls_wants_write: bool) {
4237 if tls_wants_write && !self.kernel_refused_write {
4238 self.readiness.signal_pending_write();
4239 }
4240 }
4241
4242 /// Record what a socket write of this pass answered, for
4243 /// [`Self::ensure_tls_flushed`]. Every write the shell makes reports
4244 /// here, so the flag always describes the latest one.
4245 ///
4246 /// A refused write also consumes the WRITABLE event, as
4247 /// `update_readiness_after_write` does for the writes whose status it
4248 /// reads. The empty-buffer flushes of `H2Shell::flush_tls_records` have
4249 /// no other reader of their status, so without this a refused flush left
4250 /// the edge it was called on in place, and `Mux::ready_inner` ran the
4251 /// pass again on every inner iteration.
4252 fn note_write_status(&mut self, status: SocketResult) {
4253 self.kernel_refused_write = status == SocketResult::WouldBlock;
4254 if self.kernel_refused_write {
4255 self.readiness.event.remove(Ready::WRITABLE);
4256 }
4257 }
4258
4259 /// Evict every per-stream piece of state carried by this `ConnectionH2`.
4260 ///
4261 /// **Invariant**: `stream_table` (wire map, `rst_sent`,
4262 /// `stream_last_activity_at`, `stream_fc_stalled_since`,
4263 /// `stream_fc_stalled_progress`, `expect_read`/`expect_write`) and
4264 /// `prioriser` MUST be emptied of `stream_id` here. `prioriser` is the
4265 /// only per-stream cache not folded into [`h2_stream_table::H2StreamTable`]
4266 /// — this module deliberately does not have the RFC 9218 priority map,
4267 /// same boundary [`h2_flow_control`]'s doc draws for per-stream send
4268 /// window state. `H2StreamTable::remove` asserts its own five caches are
4269 /// clean as a postcondition; forgetting `prioriser` here would still
4270 /// cause unbounded memory growth on long-lived connections with many
4271 /// cancelled streams, so it stays a second, explicit call.
4272 ///
4273 /// When the stream owns the DATA payload being read, the rest of that
4274 /// payload is skipped instead of left unread — see
4275 /// [`Self::skip_orphaned_data_payload`].
4276 fn remove_dead_stream(&mut self, stream_id: StreamId, global_stream_id: GlobalStreamId) {
4277 // Read before `H2StreamTable::remove` nulls `expect_write`: the
4278 // parked DATA dies with its stream (sozu-proxy/sozu#1641).
4279 if matches!(
4280 self.stream_table.expect_write(),
4281 Some(H2StreamId::Other { gid, .. }) if gid == global_stream_id
4282 ) {
4283 let parked_data = std::mem::take(&mut self.parked_data);
4284 if parked_data > 0 {
4285 // An overflow sends GOAWAY(FLOW_CONTROL_ERROR); its outcome
4286 // lives on in the connection state (see the callee's doc).
4287 let _ = self.refund_parked_data(parked_data);
4288 }
4289 }
4290 // Read before `H2StreamTable::remove` nulls `expect_read`.
4291 let orphaned = match (self.stream_table.expect_read(), &self.state) {
4292 (Some((H2StreamId::Other { gid, .. }, remaining)), H2State::Frame(header))
4293 if gid == global_stream_id =>
4294 {
4295 Some((remaining, header.payload_len))
4296 }
4297 _ => None,
4298 };
4299 if self.stream_table.remove(stream_id, global_stream_id)
4300 == h2_stream_table::RemoveOutcome::NotPresent
4301 {
4302 error!(
4303 "{} dead stream_id {} missing from streams map",
4304 log_context!(self),
4305 stream_id
4306 );
4307 }
4308 self.scheduler.remove_stream(&stream_id);
4309 if let Some((remaining, payload_len)) = orphaned {
4310 self.skip_orphaned_data_payload(remaining, payload_len);
4311 }
4312 }
4313
4314 /// Give the connection send window back `dropped` octets of DATA credit
4315 /// spent on frames that were prepared and then dropped before the wire
4316 /// took any of them ([`Self::parked_data`], sozu-proxy/sozu#1641), and
4317 /// re-arm WRITABLE when that reopens the window: every stream starved on
4318 /// it may have released WRITABLE, and nothing else would wake them. The
4319 /// stream's own window is the caller's, since only a live stream has one
4320 /// worth restoring.
4321 ///
4322 /// When the peer's WINDOW_UPDATEs have meanwhile pushed the window so
4323 /// high that the refund would pass 2^31-1, the credit the peer granted
4324 /// overflows (RFC 9113 §6.9.1) and this answers the GOAWAY
4325 /// (FLOW_CONTROL_ERROR) `Self::handle_window_update_frame` sends for an
4326 /// overflowing increment. `Self::goaway` records its outcome in the
4327 /// connection state, so a caller that cannot return it
4328 /// ([`Self::remove_dead_stream`]) may drop the result.
4329 fn refund_parked_data(&mut self, dropped: i32) -> Option<MuxResult> {
4330 debug_assert!(dropped > 0, "only a positive refund is applied");
4331 let window_before = self.flow_control.window();
4332 match self.flow_control.refund_send_window(dropped) {
4333 h2_flow_control::ApplyWindowUpdateOutcome::Applied {
4334 new_window,
4335 should_arm_writable,
4336 } => {
4337 if should_arm_writable {
4338 self.readiness.arm_writable();
4339 }
4340 debug_assert!(
4341 new_window > window_before,
4342 "an applied refund grew the connection window"
4343 );
4344 debug!(
4345 "{} H2 prepared DATA dropped unsent: {} octets of send credit given back, connection window {}",
4346 log_context!(self),
4347 dropped,
4348 new_window
4349 );
4350 None
4351 }
4352 h2_flow_control::ApplyWindowUpdateOutcome::Overflow => {
4353 debug_assert_eq!(
4354 self.flow_control.window(),
4355 window_before,
4356 "an overflowing refund is not applied"
4357 );
4358 error!(
4359 "{} INVALID WINDOW INCREMENT: refunding {} octets of dropped DATA passes 2^31-1",
4360 log_context!(self),
4361 dropped
4362 );
4363 Some(self.goaway(H2Error::FlowControlError))
4364 }
4365 }
4366 }
4367
4368 /// Empty the encoder's dynamic table after a header block it encoded was
4369 /// dropped unsent, and queue the size updates `0`, then the maximum size,
4370 /// for the next block, which empty the peer's table as well (RFC 7541
4371 /// §4.2; sozu-proxy/sozu#1627). The caller guarantees that no block
4372 /// encoded before this call is still queued for the wire.
4373 fn reset_encoder_table(&mut self) {
4374 let max_size = self.hpack.reset_encoder_table();
4375 let signal = u32::try_from(max_size).unwrap_or(u32::MAX);
4376 debug_assert_eq!(
4377 usize::try_from(signal).ok(),
4378 Some(max_size),
4379 "the encoder's maximum size came from a u32 setting"
4380 );
4381 self.pending_table_size_update = Some(signal);
4382 debug!(
4383 "{} H2 an encoded header block was dropped unsent: HPACK encoder table reset, next block signals 0 then {}",
4384 log_context!(self),
4385 max_size
4386 );
4387 debug_assert!(
4388 self.pending_table_size_update.is_some(),
4389 "the reset is signalled on the next header block"
4390 );
4391 }
4392
4393 /// Keep the connection reading after the stream that owned the DATA
4394 /// payload being read was removed mid-frame (idle reap, reset).
4395 ///
4396 /// Nulling `expect_read` alone left the connection in `H2State::Frame`
4397 /// with nothing to read, so it never read again (#1597). The remaining
4398 /// `remaining` bytes are read and dropped through `H2State::Discard`,
4399 /// in pieces as small as the room in `zero` allows, like HAProxy's
4400 /// "skipping remaining frame payload". `handle_data_frame` will never
4401 /// see this frame, so its whole `payload_len`, padding included, is
4402 /// credited to the connection receive window here (RFC 9113 §6.9), as
4403 /// its closed-stream branch does. READABLE interest is restored because
4404 /// the stream may have been parked on buffer pressure, and
4405 /// `try_resume_reading` only resumes a stream that still exists.
4406 fn skip_orphaned_data_payload(&mut self, remaining: usize, payload_len: u32) {
4407 let conn_threshold = self.connection_config.initial_connection_window / 2;
4408 if let Some(increment) = self
4409 .flow_control
4410 .account_received_bytes(payload_len, conn_threshold)
4411 {
4412 self.queue_window_update(0, increment);
4413 self.readiness.arm_writable();
4414 }
4415 self.state = H2State::Discard;
4416 self.discarded_field_block = None;
4417 self.stream_table
4418 .set_expect_read(Some((H2StreamId::Zero, remaining)));
4419 self.readiness.interest.insert(Ready::READABLE);
4420 }
4421
4422 /// Whether the read in progress is an orphaned DATA remainder that is
4423 /// dropped as it is read ([`Self::skip_orphaned_data_payload`]). A
4424 /// refused HEADERS discard always carries a field block.
4425 fn skipping_orphaned_payload(&self) -> bool {
4426 matches!(self.state, H2State::Discard) && self.discarded_field_block.is_none()
4427 }
4428
4429 /// Drop stream-id mappings for streams that never became active before a
4430 /// connection-level close. This happens on incomplete/oversized header
4431 /// blocks: the stream slot is created on the initial HEADERS frame, then a
4432 /// GOAWAY closes the connection before the request is fully materialized.
4433 pub fn prune_inactive_streams_while_closing<L>(&mut self, context: &mut Context<L>)
4434 where
4435 L: ListenerHandler + L7ListenerHandler,
4436 {
4437 if !self.drain.draining() || !matches!(self.state, H2State::GoAway | H2State::Error) {
4438 return;
4439 }
4440
4441 let stale_streams = self
4442 .stream_table
4443 .streams()
4444 .iter()
4445 .filter_map(|(&stream_id, &global_stream_id)| {
4446 (!context.streams[global_stream_id].state.is_open())
4447 .then_some((stream_id, global_stream_id))
4448 })
4449 .collect::<Vec<_>>();
4450
4451 for (stream_id, global_stream_id) in stale_streams {
4452 let stream = &mut context.streams[global_stream_id];
4453 if stream.state == StreamState::Idle {
4454 stream.front.clear();
4455 stream.front.storage.clear();
4456 stream.back.clear();
4457 stream.back.storage.clear();
4458 stream.metrics.reset();
4459 stream.state = StreamState::Recycle;
4460 }
4461 self.remove_dead_stream(stream_id, global_stream_id);
4462 }
4463 }
4464
4465 /// Post-write phase: check drain completion, flush TLS, and update readiness.
4466 ///
4467 /// `bytes_written_this_pass` reports the total outbound bytes `write_streams`
4468 /// pushed to the socket (across every stream), and is used to distinguish
4469 /// two very different "no `expect_write`" states:
4470 ///
4471 /// - **Voluntary yield with progress**: at least one DATA/HEADERS frame
4472 /// emitted, but a stream left non-empty `back.out`/`back.blocks` because
4473 /// the converter yielded (e.g. RFC 9218 incremental rotation). LIFECYCLE
4474 /// §9 invariant 16: keep `Ready::WRITABLE` armed so the session loop can
4475 /// resume flushing on the next tick without waiting for an external
4476 /// wake-up that edge-triggered epoll will not deliver.
4477 /// - **No progress at all**: converter pushed every block back (e.g. flow
4478 /// window exhausted, no HEADERS ready yet). Strip `Ready::WRITABLE` —
4479 /// forward progress must come from an external trigger
4480 /// (`WINDOW_UPDATE`, new request), not from looping writable().
4481 ///
4482 /// Answers [`H2FinalizeTarget`]: [`H2FinalizeTarget::Done`] in the normal
4483 /// case — `MuxResult::Continue`, or the graceful GOAWAY taken when
4484 /// draining with every stream gone — and otherwise the TLS flush step its
4485 /// caller must perform before [`Self::finalize_write_after_flush`] settles
4486 /// the pass.
4487 ///
4488 /// `tls_wants_write` is `H2Shell::tls_wants_write`'s answer, read by the
4489 /// caller. It is the pre-flush half of the query / flush / query triple
4490 /// this function used to run inline; the flush and the post-flush query
4491 /// are the caller's too, which is what leaves a body a byte-in /
4492 /// byte-out core can keep. The caller reads it before the RFC 9113 §6.8
4493 /// graceful-GOAWAY check below rather than after, which the query's
4494 /// freedom from side effects makes unobservable: on that path the value
4495 /// is discarded.
4496 pub fn finalize_write<L>(
4497 &mut self,
4498 tls_wants_write: bool,
4499 socket_write: bool,
4500 bytes_written_this_pass: usize,
4501 context: &mut Context<L>,
4502 ) -> H2FinalizeTarget
4503 where
4504 L: ListenerHandler + L7ListenerHandler,
4505 {
4506 // RFC 9113 §6.8: if draining and all streams have completed,
4507 // send the final GOAWAY with the actual last_stream_id
4508 if self.drain.draining() && self.stream_table.streams().is_empty() {
4509 return H2FinalizeTarget::Done(self.graceful_goaway(self.now));
4510 }
4511
4512 // The flush triple and the readiness policy behind it are one
4513 // decision, taken in `h2_close` where it is enumerable without a
4514 // socket. Why it is a sibling of `CloseAction` rather than four more
4515 // of its variants, why the conditional middle flush is an INPUT
4516 // instead of an `if` kept here, and why the invariant-16 probe is
4517 // passed as a closure: `h2_close::finalize_action`.
4518 // Bound rather than matched inline: the invariant-16 probe below
4519 // borrows `self.stream_table` and `context.streams`, and a `match`
4520 // keeps its scrutinee's temporaries alive for every arm — including
4521 // the arms that need `&mut self.readiness` and `&mut context.debug`.
4522 let action = h2_close::finalize_action(
4523 TlsFlushPhase::BeforeFlush,
4524 tls_wants_write,
4525 socket_write,
4526 self.stream_table.expect_write().is_some(),
4527 bytes_written_this_pass > 0,
4528 || any_stream_has_pending_back(self.stream_table.streams(), &context.streams),
4529 self.control_tx.has_pending()
4530 || !self.flow_control.pending_window_updates_is_empty()
4531 || !self.output.is_empty(),
4532 );
4533 match action {
4534 // A parked `expect_write` owns the next tick: no bit moves.
4535 FinalizeAction::Parked => H2FinalizeTarget::Done(MuxResult::Continue),
4536 // LIFECYCLE §9 invariant 16: a voluntary scheduler yield left
4537 // stranded bytes in a stream's `back.out`/`back.blocks` after a
4538 // pass that made forward progress. Retaining `Ready::WRITABLE` is
4539 // the ABSENCE of the withdrawal below, so this arm changes no bit
4540 // — it only narrates.
4541 FinalizeAction::RetainPendingBack => {
4542 #[cfg(debug_assertions)]
4543 context.debug.push(DebugEvent::Str(
4544 "finalize_write: invariant 16 retained WRITABLE (pending back-buffer)"
4545 .to_owned(),
4546 ));
4547 H2FinalizeTarget::Done(MuxResult::Continue)
4548 }
4549 // Control-frame liveness: a pass that ended before
4550 // `flush_pending_control_frames` drained the RST / WINDOW_UPDATE
4551 // queues, or with bytes still in the ordered output queue, must
4552 // keep `Ready::WRITABLE`. Without this rearm the next tick would
4553 // drop it and the queued frame would stall until an unrelated
4554 // event re-triggered writable — which is exactly the scenario
4555 // h2spec trips by sending back-to-back malformed streams. A
4556 // stalled output flush parks the pass before it gets here, so
4557 // the output leg is defence in depth (#1604).
4558 FinalizeAction::ArmControlQueue => {
4559 #[cfg(debug_assertions)]
4560 context.debug.push(DebugEvent::Str(
4561 "finalize_write: retained WRITABLE (control queue non-empty)".to_owned(),
4562 ));
4563 self.readiness.arm_writable();
4564 self.metric_events
4565 .push(MetricEvent::WritableRearmedControlQueue);
4566 H2FinalizeTarget::Done(MuxResult::Continue)
4567 }
4568 // We wrote everything.
4569 FinalizeAction::Quiesce => {
4570 #[cfg(debug_assertions)]
4571 context.debug.push(DebugEvent::Str(format!(
4572 "Wrote everything: {:?}",
4573 self.stream_table.streams()
4574 )));
4575 self.readiness.interest.remove(Ready::WRITABLE);
4576 H2FinalizeTarget::Done(MuxResult::Continue)
4577 }
4578 // The two TLS answers differ only in the step the CALLER performs,
4579 // and both come back through the single post-flush query in
4580 // `Self::finalize_write_after_flush`.
4581 FinalizeAction::Flush => H2FinalizeTarget::Flush,
4582 FinalizeAction::SkipFlush => H2FinalizeTarget::SkipFlush,
4583 // Named rather than `other =>`: a wildcard arm would turn a new
4584 // `FinalizeAction` variant into a release-mode panic on the proxy
4585 // write path instead of a compile error.
4586 action @ (FinalizeAction::ReArm | FinalizeAction::Settled) => {
4587 unreachable!("BeforeFlush yielded {action:?}")
4588 }
4589 }
4590 }
4591
4592 /// Settle a write pass whose [`H2FinalizeTarget`] asked for the TLS flush
4593 /// step, given the answer `H2Shell::tls_wants_write` gave AFTER it.
4594 ///
4595 /// The second half of [`Self::finalize_write`]'s protocol, and the reason
4596 /// the split is not a rename: `tls_wants_write` here is a DIFFERENT
4597 /// question from the one that produced [`H2FinalizeTarget::Flush`] or
4598 /// [`H2FinalizeTarget::SkipFlush`]. The first asks whether rustls holds
4599 /// records; this one asks whether the step between them landed —
4600 /// `socket_write(&[])`'s `(size, status)` is discarded at the call site,
4601 /// so nothing else on this path can tell. A caller that reused the first
4602 /// answer here would re-arm forever or never, and either one strands the
4603 /// connection under edge-triggered epoll.
4604 ///
4605 /// The readiness policy of [`Self::finalize_write`] is deliberately NOT
4606 /// reopened: once records were found pending, this pass leaves the bits to
4607 /// the next one, so the degenerate arguments below are the shape
4608 /// `h2_close::goaway_close_action`'s own post-flush call already uses —
4609 /// the inputs the flush could not change are not re-read.
4610 pub fn finalize_write_after_flush(&mut self, tls_wants_write: bool) -> MuxResult {
4611 let action = h2_close::finalize_action(
4612 TlsFlushPhase::AfterFlush,
4613 tls_wants_write,
4614 false,
4615 false,
4616 false,
4617 || false,
4618 false,
4619 );
4620 match action {
4621 // The post-flush answer is read once, by the caller, and feeds
4622 // both the decision and the re-arm: they are one question asked at
4623 // one point, and nothing mutates the socket between them. The
4624 // re-arm still goes through `ensure_tls_flushed` rather than
4625 // touching `signal_pending_write` here, which keeps every
4626 // post-decision TLS re-arm in this file spelled the same way, as
4627 // the GoAway and Error arms in `dispatch_writable_state` and
4628 // `dispatch_writable_state_after_flush` already do.
4629 FinalizeAction::ReArm => self.ensure_tls_flushed(tls_wants_write),
4630 FinalizeAction::Settled => {}
4631 action @ (FinalizeAction::Flush
4632 | FinalizeAction::SkipFlush
4633 | FinalizeAction::Parked
4634 | FinalizeAction::RetainPendingBack
4635 | FinalizeAction::ArmControlQueue
4636 | FinalizeAction::Quiesce) => {
4637 unreachable!("AfterFlush yielded {action:?}")
4638 }
4639 }
4640 MuxResult::Continue
4641 }
4642
4643 /// Queue pending control frames (WINDOW_UPDATEs, RST_STREAMs) behind the
4644 /// ordered output and flush it, before entering the main writable state
4645 /// machine.
4646 ///
4647 /// Answers [`H2ControlFlushTarget`]: [`H2ControlFlushTarget::Proceed`]
4648 /// when `writable()` should carry on into its state dispatch,
4649 /// [`H2ControlFlushTarget::Done`] when a GOAWAY already ended the pass,
4650 /// [`H2ControlFlushTarget::FlushOutput`] when [`Self::output_pending`]
4651 /// holds bytes for the caller to move, and
4652 /// [`H2ControlFlushTarget::Stalled`] when that move stalled — in which
4653 /// case the caller reads `tls_wants_write` and hands it to
4654 /// [`Self::ensure_tls_flushed`].
4655 ///
4656 /// The socket write is the caller's, as everywhere on this write path:
4657 /// the core answers a step, the caller performs the socket work, and the
4658 /// core is told what the socket said, through
4659 /// [`Self::handle_control_flush`].
4660 ///
4661 /// There is one flush, at the end. Every stage appends whole frames to
4662 /// the one ordered output queue (#1604), so a stage no longer has to wait
4663 /// for an earlier one's bytes to leave a shared buffer before it can
4664 /// serialise, and whatever was already queued — an ACK, the rest of a
4665 /// stream frame a partial write cut — stays ahead of what this walk adds.
4666 pub fn flush_pending_control_frames(&mut self) -> H2ControlFlushTarget {
4667 // The peer hung up while the connection drains: nothing queued for it
4668 // will be read. Only OUTPUT is dropped. `zero` is input only, and a
4669 // `Ready::HUP` can be a TCP half-close with frames the peer already
4670 // sent still unread (review of e1c3c2fb, B2), which the shutdown
4671 // drive's forced `readable()` goes on to read.
4672 if self.frontend_hung_up_while_draining() {
4673 self.stream_table.set_expect_write(None);
4674 self.clear_output();
4675 self.flow_control.clear_pending_window_updates();
4676 self.control_tx.clear_pending();
4677 }
4678
4679 // RFC 9113 §6.5: check if peer has timed out on SETTINGS ACK
4680 if let Some(sent_at) = self.settings_sent_at
4681 && self.now.saturating_duration_since(sent_at) >= SETTINGS_ACK_TIMEOUT
4682 {
4683 warn!(
4684 "{} SETTINGS ACK timeout: no SETTINGS ACK observed within {:?}",
4685 log_context!(self),
4686 SETTINGS_ACK_TIMEOUT
4687 );
4688 return H2ControlFlushTarget::Done(self.goaway(H2Error::SettingsTimeout));
4689 }
4690
4691 // Stage — queue pending WINDOW_UPDATE frames.
4692 // Queued inline to avoid extra event loop iterations that could cause
4693 // response data to be sent before subsequent frames are validated.
4694 let pending = self.flow_control.pending_window_updates_len();
4695 if pending > 0 {
4696 let flow_control = &mut self.flow_control;
4697 let mut frames_written = 0;
4698 // Emission order is the deterministic ascending stream_id order
4699 // `H2FlowControl` guarantees — see its module doc — not
4700 // insertion/arrival order.
4701 let _ = self
4702 .output
4703 .push_frames(pending * WINDOW_UPDATE_FRAME_SIZE, |buf| {
4704 let (offset, frames) = flow_control.drain_window_updates_into(buf);
4705 frames_written = frames;
4706 Ok::<_, std::convert::Infallible>(offset)
4707 });
4708 if frames_written > 0 {
4709 self.metric_events
4710 .push(MetricEvent::WindowUpdateFramesSent(frames_written as i64));
4711 }
4712 }
4713
4714 // Stage — RST_STREAM overflow check + queue.
4715 // The pending queue is bounded; an RST it had to refuse never reaches
4716 // the wire, so the peer would believe that stream still live. Escalate
4717 // before serialising anything. This is a bound on what is pending, not
4718 // a lifetime cap: resets drained as they come never trip it, and the
4719 // peer-provoked ones are capped by the flood detector instead.
4720 if !matches!(self.state, H2State::GoAway | H2State::Error) && self.control_tx.overflowed() {
4721 error!(
4722 "{} pending RST_STREAM queue overflowed its bound {} ({} queued over the connection), sending GOAWAY(ENHANCE_YOUR_CALM)",
4723 log_context!(self),
4724 self.control_tx.max_pending(),
4725 self.control_tx.lifetime_queued(),
4726 );
4727 return H2ControlFlushTarget::Done(self.goaway(H2Error::EnhanceYourCalm));
4728 }
4729
4730 // Queue pending RST_STREAM frames (queued when refusing streams).
4731 // Accounting happens at queue-time inside `Self::enqueue_rst`, so this
4732 // only serialises — no metric/flood calls here would double-count.
4733 // Emission order is queue order, which is arrival order over
4734 // already-deduped stream ids — see `h2_control_tx`'s module doc.
4735 let pending = self.control_tx.pending_len();
4736 if pending > 0 {
4737 let control_tx = &mut self.control_tx;
4738 let _ = self
4739 .output
4740 .push_frames(pending * RST_STREAM_FRAME_SIZE, |buf| {
4741 let (offset, _frames_written) = control_tx.drain_rst_streams_into(buf);
4742 Ok::<_, std::convert::Infallible>(offset)
4743 });
4744 }
4745
4746 if self.output.is_empty() {
4747 H2ControlFlushTarget::Proceed
4748 } else {
4749 H2ControlFlushTarget::FlushOutput
4750 }
4751 }
4752
4753 /// Answer the [`H2ControlFlushTarget::FlushOutput`] the caller just
4754 /// performed: `stalled` is what the caller's write reported.
4755 pub fn handle_control_flush(&mut self, stalled: bool) -> H2ControlFlushTarget {
4756 if stalled {
4757 H2ControlFlushTarget::Stalled
4758 } else {
4759 H2ControlFlushTarget::Proceed
4760 }
4761 }
4762
4763 /// Take `writable()`'s `(H2State, Position)` decision, given the answer
4764 /// `H2Shell::tls_wants_write` gave AFTER the pass preamble's flush.
4765 ///
4766 /// `tls_wants_write` is that answer, read by the caller. It is the third
4767 /// step of the preamble's own query / flush / query triple and the
4768 /// pre-flush question both close arms decide on: the
4769 /// `(H2State::Error, Position::Server)` arm has no flush of its own and
4770 /// reads nothing else, while the `H2State::GoAway` arm asks for a second
4771 /// flush through [`H2WritableStateTarget::Flush`]. ONE read serves
4772 /// whichever of the two runs — only one of them can — and nothing mutates
4773 /// the socket between the caller's read and this call. The arms that never
4774 /// consult it discard it, exactly as `H2Shell::write_streams` discards the
4775 /// answer it reads before [`Self::finalize_write`]'s graceful-GOAWAY
4776 /// check; `H2Shell::tls_wants_write` is free of side effects, which is what
4777 /// makes both unobservable.
4778 ///
4779 /// Answers [`H2WritableStateTarget`]. Every arm but two ends the pass with
4780 /// [`H2WritableStateTarget::Done`]; the `H2State::GoAway` arm may ask for
4781 /// the TLS flush step, and the four proxying states name
4782 /// [`H2WritableStateTarget::WriteStreams`]. That last one is the caller's
4783 /// to run for the same reason the flush is — `H2Shell::write_streams` owns
4784 /// the socket and this function does not — and it is why this function
4785 /// takes neither `Context` nor `Endpoint`: no other arm reads either.
4786 pub fn dispatch_writable_state(&mut self, tls_wants_write: bool) -> H2WritableStateTarget {
4787 match (&self.state, &self.position) {
4788 (H2State::Error, Position::Server) => {
4789 // The caller's preamble already attempted this pass's flush,
4790 // so this arm decides on the post-flush answer and has no
4791 // `Flush` of its own — see `h2_close`'s module doc.
4792 match h2_close::error_close_action(tls_wants_write) {
4793 CloseAction::ReArmAndContinue => {
4794 self.ensure_tls_flushed(tls_wants_write);
4795 H2WritableStateTarget::Done(MuxResult::Continue)
4796 }
4797 CloseAction::CloseSession => {
4798 H2WritableStateTarget::Done(MuxResult::CloseSession)
4799 }
4800 // Named rather than `other =>`: a wildcard arm would turn a
4801 // new `CloseAction` variant into a release-mode panic in the
4802 // proxy write path instead of a compile error.
4803 action @ (CloseAction::Flush | CloseAction::Disconnect) => {
4804 unreachable!("error_close_action yielded {action:?}")
4805 }
4806 }
4807 }
4808 // The preface and SETTINGS are out and the server's SETTINGS are
4809 // not read yet: there is nothing to write until they are. A write
4810 // readiness here is not an error — `start_stream` arms one for a
4811 // stream linked to this connection, which `Router::plan_connect`'s
4812 // pool scan does for a `Connected` connection whatever its handshake
4813 // state — so it is withdrawn, and the SETTINGS ACK the read queues
4814 // re-arms it (sozu-proxy/sozu#1631). Only the control-frame preamble,
4815 // which already ran, may have written: stream frames wait for `Header`.
4816 (H2State::ServerSettings, Position::Client(..)) => {
4817 trace!(
4818 "{} waiting for the server SETTINGS before writing streams",
4819 log_context!(self)
4820 );
4821 self.readiness.interest.remove(Ready::WRITABLE);
4822 debug_assert!(
4823 !self.readiness.interest.is_writable(),
4824 "a backend awaiting the server SETTINGS must stop asking to write"
4825 );
4826 debug_assert!(
4827 self.readiness.interest.is_readable(),
4828 "a backend awaiting the server SETTINGS must keep reading them"
4829 );
4830 H2WritableStateTarget::Done(MuxResult::Continue)
4831 }
4832 (H2State::Error, _) | (H2State::ClientSettings, Position::Server) => {
4833 error!(
4834 "{} Unexpected combination: (Writable, {:?}, {:?})",
4835 log_context!(self),
4836 self.state,
4837 self.position
4838 );
4839 H2WritableStateTarget::Done(self.force_disconnect())
4840 }
4841 (H2State::ClientPreface, Position::Server) => {
4842 H2WritableStateTarget::Done(MuxResult::Continue)
4843 }
4844 // Discard state: pending data (e.g. RST_STREAM) was already
4845 // written in the caller's control-frame preamble; let the readable
4846 // path consume the remaining frame payload.
4847 (H2State::Discard, _) => H2WritableStateTarget::Done(MuxResult::Continue),
4848 // A stream parked by a stalled write still has whole frames
4849 // queued. The rest of the frame the stall cut, if any, already
4850 // went with the GOAWAY from the ordered output (#1604); finish
4851 // the others before the close decision, since a GOAWAY does not
4852 // close the streams at or below its last stream id (RFC 9113
4853 // §6.8).
4854 (H2State::GoAway, _)
4855 if matches!(
4856 self.stream_table.expect_write(),
4857 Some(H2StreamId::Other { .. })
4858 ) =>
4859 {
4860 H2WritableStateTarget::WriteStreams
4861 }
4862 (H2State::GoAway, _) => {
4863 // Response DATA frames may still sit in rustls's output
4864 // buffer — accepted by socket_write_vectored during
4865 // write_streams() but not yet flushed to TCP. Under TCP
4866 // backpressure (HAProxy chain) this is the primary truncation
4867 // vector, so the decision lives in `h2_close`, exhaustively
4868 // unit-tested there rather than inline here. Why the two
4869 // `socket_wants_write()` queries are two DIFFERENT questions,
4870 // and why the flush between them stays inside the same
4871 // `writable()` call: see `h2_close::TlsFlushPhase`. The flush
4872 // and the query after it belong to the caller, and
4873 // `H2WritableStateTarget::Flush` is how this arm asks for them.
4874 match h2_close::goaway_close_action(
4875 TlsFlushPhase::BeforeFlush,
4876 self.peer_gone_after_final_goaway(),
4877 tls_wants_write,
4878 ) {
4879 CloseAction::CloseSession => {
4880 H2WritableStateTarget::Done(MuxResult::CloseSession)
4881 }
4882 CloseAction::Flush => H2WritableStateTarget::Flush,
4883 CloseAction::Disconnect => H2WritableStateTarget::Done(self.force_disconnect()),
4884 action @ CloseAction::ReArmAndContinue => {
4885 unreachable!("BeforeFlush yielded {action:?}")
4886 }
4887 }
4888 }
4889 (H2State::ClientPreface, Position::Client(..)) => {
4890 trace!("{} Preparing preface and settings", log_context!(self));
4891 // RFC 9113 §3.4: the preface is the first byte on the wire.
4892 // No stream frame can be queued before it, because no stream
4893 // opens before `Header` (sozu-proxy/sozu#1631).
4894 debug_assert!(
4895 self.output.is_empty(),
4896 "nothing may be queued ahead of the client connection preface"
4897 );
4898 self.output.push(serializer::H2_PRI.as_bytes());
4899 let local_settings = &self.local_settings;
4900 match self.output.push_frames(SETTINGS_FRAME_SIZE, |buf| {
4901 serializer::gen_settings(buf, local_settings).map(|(_, size)| size)
4902 }) {
4903 Ok(_) => {
4904 self.metric_events.push(MetricEvent::SettingsFrameSent);
4905 // RFC 9113 §6.5: start tracking SETTINGS ACK timeout
4906 self.settings_sent_at = Some(self.now);
4907 }
4908 Err(error) => {
4909 error!(
4910 "{} Could not serialize SettingsFrame: {:?}",
4911 log_context!(self),
4912 error
4913 );
4914 return H2WritableStateTarget::Done(self.force_disconnect());
4915 }
4916 };
4917
4918 self.state = H2State::ClientSettings;
4919 self.readiness.signal_pending_write();
4920 H2WritableStateTarget::Done(MuxResult::Continue)
4921 }
4922 (H2State::ClientSettings, Position::Client(..)) => {
4923 trace!("{} Sent preface and settings", log_context!(self));
4924 self.state = H2State::ServerSettings;
4925 self.stream_table
4926 .set_expect_read(Some((H2StreamId::Zero, 9)));
4927 // A backend connection starts write-only
4928 // (`Connection::new_h2_client`); it reads from here on, for
4929 // the server's SETTINGS. The preamble's output flush used to
4930 // turn READABLE on as a side effect of draining `zero`.
4931 self.readiness.interest.insert(Ready::READABLE);
4932 self.readiness.interest.remove(Ready::WRITABLE);
4933 H2WritableStateTarget::Done(MuxResult::Continue)
4934 }
4935 (H2State::ServerSettings, Position::Server) => {
4936 // The connection-window enlargement already left with the
4937 // SETTINGS it follows: the `(ClientSettings, Server)` readable
4938 // arm serialises both into `zero`, which the preamble above
4939 // drained in one write.
4940 self.expect_header();
4941 H2WritableStateTarget::Done(MuxResult::Continue)
4942 }
4943 // Proxying states — writing application data (request/response).
4944 // These arms are NOT a filter on what the pass emits: a connection
4945 // whose only queued output is a PING or SETTINGS acknowledgement is
4946 // in `H2State::Header` too, so reaching `write_streams` never meant
4947 // "application data is being written". The frontend timeout is
4948 // therefore armed by `ConnectionH2::handle_write`, per stream
4949 // transmit that moved bytes, and not on the way in here.
4950 (H2State::Header, _)
4951 | (H2State::Frame(_), _)
4952 | (H2State::ContinuationFrame(_), _)
4953 | (H2State::ContinuationHeader(_), _) => H2WritableStateTarget::WriteStreams,
4954 }
4955 }
4956
4957 /// Settle the `H2State::GoAway` arm whose [`H2WritableStateTarget::Flush`]
4958 /// asked for the TLS flush step, given the answer
4959 /// `H2Shell::tls_wants_write` gave AFTER it.
4960 ///
4961 /// The second half of [`Self::dispatch_writable_state`]'s protocol, built
4962 /// the way [`Self::finalize_write_after_flush`] is built and for the same
4963 /// reason: `tls_wants_write` here is a DIFFERENT question from the one
4964 /// that arm decided on. The first asks whether rustls holds records; this
4965 /// one asks whether the flush between them landed —
4966 /// `socket_write(&[])`'s `(size, status)` is discarded at the call site,
4967 /// so nothing else on this path can tell. A caller that reused the first
4968 /// answer here would close a TLS connection with records still pending,
4969 /// and the peer would read a truncated response; that is the truncation
4970 /// vector `h2_close`'s module doc names, not a style point.
4971 ///
4972 /// `peer_gone` is deliberately not re-read — the degenerate `false` below
4973 /// is `h2_close::goaway_close_action`'s own post-flush convention — for
4974 /// the same reason [`Self::finalize_write_after_flush`] does not reopen
4975 /// the readiness policy: the flush cannot resurrect a departed peer, and
4976 /// the decision answers from the one input it could have changed.
4977 pub fn dispatch_writable_state_after_flush(&mut self, tls_wants_write: bool) -> MuxResult {
4978 match h2_close::goaway_close_action(TlsFlushPhase::AfterFlush, false, tls_wants_write) {
4979 CloseAction::ReArmAndContinue => {
4980 self.ensure_tls_flushed(tls_wants_write);
4981 MuxResult::Continue
4982 }
4983 CloseAction::Disconnect => self.force_disconnect(),
4984 // Named rather than `other =>`: a wildcard arm would turn a new
4985 // `CloseAction` variant into a release-mode panic in the proxy
4986 // write path instead of a compile error.
4987 action @ (CloseAction::CloseSession | CloseAction::Flush) => {
4988 unreachable!("AfterFlush yielded {action:?}")
4989 }
4990 }
4991 }
4992
4993 /// Report the access-log RTTs for the local frontend and the linked backend.
4994 ///
4995 /// Both cells are asked of `endpoint`, at the moment the stream is logged
4996 /// and at no other time. `client_rtt` comes from [`Endpoint::local_rtt`]:
4997 /// `ShellEndpoint` samples the frontend socket at the first stream logged
4998 /// on the connection and hands every later stream that one value, so
4999 /// every stream of a connection reports one number (issue #1339 Q11
5000 /// asked for one per pass), and a connection that logs nothing reads
5001 /// nothing (issue #1590). `server_rtt` comes from [`Endpoint::peer_rtt`],
5002 /// the backend connection's own one sample. `doc/configure.md` states
5003 /// both for operators reading the log.
5004 ///
5005 /// Nothing in this method reaches a socket, which is the point: the local
5006 /// RTT was the last `SocketHandler::socket_ref` read in this file, and the
5007 /// shell that owns the socket answers it now.
5008 ///
5009 /// `Position::Server`-only. On a backend H2 connection (`Position::Client`)
5010 /// the report would write swapped values onto the shared `Stream.metrics`:
5011 /// `Mux` samples the frontend into the frontend connection alone, and the
5012 /// corresponding `EndpointServer::peer_rtt` returns that same frontend, so
5013 /// the per-stream `client_rtt`/`server_rtt` cells would be populated with
5014 /// mislabelled values. Gating keeps backend H2 from poisoning the
5015 /// access-log metric for the matching frontend stream.
5016 ///
5017 /// Callers must invoke this BEFORE `endpoint.end_stream(...)` on reset
5018 /// paths so the backend lookup does not depend on
5019 /// `EndpointClient::end_stream` continuing to leave entries in
5020 /// `Router.backends`.
5021 fn snapshot_rtts<E: Endpoint>(
5022 &self,
5023 endpoint: &E,
5024 linked_token: Option<mio::Token>,
5025 ) -> (Option<Duration>, Option<Duration>) {
5026 if !self.position.is_server() {
5027 return (None, None);
5028 }
5029 (
5030 endpoint.local_rtt(),
5031 linked_token.and_then(|t| endpoint.peer_rtt(t)),
5032 )
5033 }
5034
5035 /// Try to recycle a completed server-side stream by distributing overhead,
5036 /// generating access logs, and transitioning the stream to `Recycle` state.
5037 ///
5038 /// Returns `Some((stream_id, Option<token>))` if the stream was recycled, so the
5039 /// caller can add `stream_id` to the dead-streams list and call `endpoint.end_stream()`
5040 /// if a token was returned. Returns `None` if recycling was deferred or not applicable.
5041 ///
5042 /// `client_rtt`/`server_rtt` are snapshotted by the caller — which must
5043 /// do so BEFORE `endpoint.end_stream(...)`, see [`Self::snapshot_rtts`] —
5044 /// and forwarded into the access log. `stream` stays a parameter rather
5045 /// than being looked up here because the caller already holds it as
5046 /// `&mut context.streams[global_stream_id]`.
5047 #[allow(clippy::too_many_arguments)]
5048 fn try_recycle_server_stream<L>(
5049 &mut self,
5050 stream: &mut crate::protocol::mux::Stream,
5051 global_stream_id: GlobalStreamId,
5052 stream_id: StreamId,
5053 byte_totals: (usize, usize),
5054 debug: &mut DebugHistory,
5055 listener: std::rc::Rc<std::cell::RefCell<L>>,
5056 client_rtt: Option<Duration>,
5057 server_rtt: Option<Duration>,
5058 ) -> Option<(StreamId, Option<mio::Token>)>
5059 where
5060 L: ListenerHandler + L7ListenerHandler,
5061 {
5062 match self.position {
5063 Position::Client(..) => None,
5064 Position::Server => {
5065 // Already logged by a reset path; retire the stream after its RST is flushed.
5066 if stream.metrics.start.is_none() {
5067 let state = std::mem::replace(&mut stream.state, StreamState::Recycle);
5068 return match state {
5069 StreamState::Linked(token) => Some((stream_id, Some(token))),
5070 _ => Some((stream_id, None)),
5071 };
5072 }
5073
5074 // Don't recycle if the client hasn't sent END_STREAM yet —
5075 // more DATA frames may arrive for this stream.
5076 if !stream.front_received_end_of_stream {
5077 trace!(
5078 "{} Defer recycle stream {}: client still sending",
5079 log_module_context!(),
5080 global_stream_id
5081 );
5082 return None;
5083 }
5084 let stream_bytes = (
5085 stream.metrics.bin + stream.metrics.backend_bin,
5086 stream.metrics.bout + stream.metrics.backend_bout,
5087 );
5088 let active_streams = self.stream_table.streams().len();
5089 distribute_overhead(
5090 &mut stream.metrics,
5091 &mut self.bytes.overhead_bin,
5092 &mut self.bytes.overhead_bout,
5093 stream_bytes,
5094 byte_totals,
5095 active_streams,
5096 active_streams == 1,
5097 );
5098 debug.push(DebugEvent::StreamEvent(4, global_stream_id));
5099 trace!(
5100 "{} Recycle stream: {}",
5101 log_module_context!(),
5102 global_stream_id
5103 );
5104 self.metric_events.push(MetricEvent::EndToEndH2Request);
5105 if routed_to_a_backend(stream) {
5106 self.flood_detector.record_stream_opened();
5107 }
5108 let (token, events) =
5109 Self::complete_server_stream(stream, listener, client_rtt, server_rtt);
5110 self.metric_events.extend(events.into_iter().flatten());
5111 Some((stream_id, token))
5112 }
5113 }
5114 }
5115
5116 /// Finalize a server-side stream after its response has been fully written.
5117 ///
5118 /// Generates an access log, resets metrics, and transitions the stream to `Recycle`.
5119 /// Returns the backend token if the stream was `Linked`, so the caller can call
5120 /// `endpoint.end_stream()` with the full `Context` (which can't be passed here
5121 /// because `stream` borrows from `context.streams`).
5122 ///
5123 /// Callers must distribute overhead *before* calling this: it resets
5124 /// `stream.metrics`, so a share credited afterwards would be discarded.
5125 fn complete_server_stream<L>(
5126 stream: &mut crate::protocol::mux::Stream,
5127 listener: std::rc::Rc<std::cell::RefCell<L>>,
5128 client_rtt: Option<Duration>,
5129 server_rtt: Option<Duration>,
5130 ) -> (Option<mio::Token>, [Option<MetricEvent>; 2])
5131 where
5132 L: ListenerHandler + L7ListenerHandler,
5133 {
5134 stream.metrics.backend_stop();
5135 let events = stream.generate_access_log(
5136 false,
5137 Some("H2::Complete"),
5138 listener,
5139 client_rtt,
5140 server_rtt,
5141 );
5142 stream.metrics.reset();
5143 let state = std::mem::replace(&mut stream.state, StreamState::Recycle);
5144 let token = if let StreamState::Linked(token) = state {
5145 Some(token)
5146 } else {
5147 None
5148 };
5149 (token, events)
5150 }
5151
5152 /// Compute the total bytes transferred across all active streams.
5153 ///
5154 /// Returns `(total_bytes_in, total_bytes_out)` where bytes_in = `bin + backend_bin`
5155 /// and bytes_out = `bout + backend_bout` for each stream.
5156 pub fn compute_stream_byte_totals<L: ListenerHandler + L7ListenerHandler>(
5157 &self,
5158 context: &Context<L>,
5159 ) -> (usize, usize) {
5160 let mut total_in = 0usize;
5161 let mut total_out = 0usize;
5162 for &gid in self.stream_table.streams().values() {
5163 let m = &context.streams[gid].metrics;
5164 total_in += m.bin + m.backend_bin;
5165 total_out += m.bout + m.backend_bout;
5166 }
5167 (total_in, total_out)
5168 }
5169
5170 /// Distribute connection-level byte overhead proportionally to a single stream.
5171 ///
5172 /// `totals` should be pre-computed via [`compute_stream_byte_totals`] **before**
5173 /// taking a mutable borrow on the target stream, to avoid borrow conflicts.
5174 /// Delegates to the free function [`distribute_overhead`].
5175 fn distribute_overhead(&mut self, metrics: &mut SessionMetrics, totals: (usize, usize)) {
5176 let stream_bytes = (
5177 metrics.bin + metrics.backend_bin,
5178 metrics.bout + metrics.backend_bout,
5179 );
5180 distribute_overhead(
5181 metrics,
5182 &mut self.bytes.overhead_bin,
5183 &mut self.bytes.overhead_bout,
5184 stream_bytes,
5185 totals,
5186 self.stream_table.streams().len(),
5187 self.stream_table.streams().len() <= 1,
5188 );
5189 }
5190
5191 /// Attribute accumulated `zero_bytes_read` to the stream or to connection overhead.
5192 fn attribute_bytes_to_stream(&mut self, metrics: &mut SessionMetrics) {
5193 self.position
5194 .count_bytes_in(metrics, self.bytes.zero_bytes_read);
5195 self.bytes.zero_bytes_read = 0;
5196 }
5197
5198 fn attribute_bytes_to_overhead(&mut self) {
5199 self.bytes.overhead_bin += self.bytes.zero_bytes_read;
5200 self.bytes.zero_bytes_read = 0;
5201 }
5202
5203 /// Queue a WINDOW_UPDATE, coalescing with any existing entry for the same stream_id.
5204 /// RFC 9113 §6.9.1: window size increment MUST be 1..2^31-1 (0x7FFFFFFF);
5205 /// `increment == 0` is a legal no-op to *send* and queues nothing (see
5206 /// `h2_flow_control::H2FlowControl::queue_window_update`).
5207 ///
5208 /// Always signals pending write so callers don't have to remember the
5209 /// edge-triggered epoll invariant:
5210 /// under ET epoll a queued WINDOW_UPDATE without a live WRITABLE event bit
5211 /// is invisible to filter_interest() and will never get flushed.
5212 fn queue_window_update(&mut self, stream_id: u32, increment: u32) {
5213 match self.flow_control.queue_window_update(
5214 stream_id,
5215 increment,
5216 self.max_pending_window_updates,
5217 ) {
5218 h2_flow_control::QueueWindowUpdateOutcome::Coalesced { old, new } => {
5219 trace!(
5220 "{} WINDOW_UPDATE coalesced: stream={} old={} new={}",
5221 log_context!(self),
5222 stream_id,
5223 old,
5224 new
5225 );
5226 }
5227 h2_flow_control::QueueWindowUpdateOutcome::Inserted { increment } => {
5228 trace!(
5229 "{} WINDOW_UPDATE queued: stream={} increment={}",
5230 log_context!(self),
5231 stream_id,
5232 increment
5233 );
5234 }
5235 h2_flow_control::QueueWindowUpdateOutcome::Dropped => {
5236 error!(
5237 "{} WINDOW_UPDATE dropped: queue full ({} entries), stream={} increment={}",
5238 log_context!(self),
5239 self.max_pending_window_updates,
5240 stream_id,
5241 increment
5242 );
5243 self.metric_events.push(MetricEvent::WindowUpdateDropped);
5244 }
5245 // Zero increment: nothing was queued, nothing to log.
5246 h2_flow_control::QueueWindowUpdateOutcome::Noop => {}
5247 }
5248 self.readiness.arm_writable();
5249 }
5250
5251 /// Re-enable READABLE if this connection is parked waiting for buffer space
5252 /// and the target stream's buffer now has enough room.
5253 ///
5254 /// This is the cross-readiness counterpart to the same-connection check in
5255 /// `writable()`. When the *other side* of a stream (frontend or backend)
5256 /// drains data via its own `writable()`, it frees buffer space that this
5257 /// connection was waiting for. Without this explicit wake-up the connection
5258 /// stays parked and the session deadlocks until a timeout fires.
5259 ///
5260 /// Returns `true` if READABLE was re-enabled.
5261 pub fn try_resume_reading<L>(&mut self, context: &Context<L>) -> bool
5262 where
5263 L: ListenerHandler + L7ListenerHandler,
5264 {
5265 if let Some((
5266 H2StreamId::Other {
5267 gid: global_stream_id,
5268 ..
5269 },
5270 amount,
5271 )) = self.stream_table.expect_read()
5272 {
5273 let stream = &context.streams[global_stream_id];
5274 let kawa = match self.position {
5275 Position::Client(..) => &stream.back,
5276 Position::Server => &stream.front,
5277 };
5278 if kawa.storage.available_space() >= amount {
5279 self.readiness.interest.insert(Ready::READABLE);
5280 return true;
5281 }
5282 }
5283 false
5284 }
5285
5286 /// Mark a stream's position-appropriate end-of-stream flag.
5287 ///
5288 /// Server reads from the front (client), so sets `front_received_end_of_stream`.
5289 /// Client reads from the back (backend), so sets `back_received_end_of_stream`.
5290 fn mark_end_of_stream(&self, stream: &mut crate::protocol::mux::Stream) {
5291 if self.position.is_server() {
5292 stream.front_received_end_of_stream = true;
5293 } else {
5294 stream.back_received_end_of_stream = true;
5295 }
5296 }
5297
5298 /// Cancel streams that have been idle longer than [`Self::stream_idle_timeout`].
5299 ///
5300 /// A stream is considered idle when no meaningful application data (non-empty
5301 /// DATA frames or HEADERS) has been received since the last activity timestamp
5302 /// in `H2StreamTable::stream_last_activity_at`.
5303 ///
5304 /// Mitigates slow-multiplex Slowloris (Pass 4 Medium #3): the connection-level
5305 /// idle timer resets on every frame, so a peer sending periodic control frames
5306 /// can pin `max_concurrent_streams` slots for the full nominal connection timeout.
5307 /// Per-stream idle deadlines guarantee each stream terminates if it stops making
5308 /// forward progress, regardless of connection-level liveness.
5309 ///
5310 /// Timed-out streams receive RST_STREAM(CANCEL) and are immediately removed
5311 /// from the streams map so they no longer count against MAX_CONCURRENT_STREAMS.
5312 /// Backend endpoints are notified and metrics are finalized.
5313 pub fn cancel_timed_out_streams<E, L>(&mut self, context: &mut Context<L>, endpoint: &mut E)
5314 where
5315 E: Endpoint,
5316 L: ListenerHandler + L7ListenerHandler,
5317 {
5318 // Entry point: `Mux::timeout` reaches this directly, without going
5319 // through `readable`, so adopt the mux's snapshot here too.
5320 self.now = context.now;
5321 // Per-connection scratch Vecs (`converter_buf`, `lowercase_buf`,
5322 // `cookie_buf`, and the scheduler's own pass-order buffer) grow to a
5323 // high-water mark and never shrink. On a long-lived idle H2
5324 // connection that briefly carried a flurry of large headers, the
5325 // backing memory stays pinned indefinitely. Reclaim past
5326 // `SCRATCH_BUF_RETAIN` when the connection has live streams but
5327 // each scratch buffer holds 4× the cap. Quiet-time only — runs
5328 // at the top of every `cancel_timed_out_streams` invocation
5329 // (which is itself called from the readable hot loop, but only
5330 // on a session that has been idle long enough to risk timing
5331 // out a stream).
5332 const SCRATCH_BUF_RETAIN: usize = 16 * 1024;
5333 self.hpack.reclaim_idle_buffers(SCRATCH_BUF_RETAIN);
5334 self.scheduler.reclaim_idle_buffer(SCRATCH_BUF_RETAIN);
5335
5336 if self.stream_table.is_empty()
5337 || (self.stream_table.activity_is_empty() && self.stream_table.fc_stall_is_empty())
5338 {
5339 return;
5340 }
5341 let now = self.now;
5342 let deadline = self.stream_idle_timeout;
5343 // Two independent per-stream guards reap on the same deadline — see
5344 // `H2StreamTable::collect_timed_out`. The flow-control-stall guard
5345 // (`stream_fc_stalled_since`) closes the HTTP/2 window-stall vector that
5346 // the bidirectional liveness guard (`stream_last_activity_at`) misses,
5347 // because an inbound DATA drip keeps the liveness timer warm while the
5348 // response stays window-blocked.
5349 let timed_out = self.stream_table.collect_timed_out(now, deadline);
5350 if timed_out.is_empty() {
5351 return;
5352 }
5353 for (sid, reason) in timed_out {
5354 info!(
5355 "{} H2 stream {} exceeded {:?} ({}), cancelling",
5356 log_context!(self),
5357 sid,
5358 deadline,
5359 reason
5360 );
5361 // M1: break reaps down by guard so a window-stall reap (a DoS
5362 // mitigation) is distinguishable from an ordinary idle reap on a
5363 // dashboard. M2: a window-stall reap whose stream dribbled some
5364 // outbound progress (`acc > 0`) below the floor is specifically a
5365 // stall-budget reap — the `WINDOW_UPDATE`-drip vector the budget
5366 // closes — counted as a subset. Read the accumulator BEFORE
5367 // `remove_dead_stream` evicts it below.
5368 match reason {
5369 "H2::WindowStall" => {
5370 self.metric_events
5371 .push(MetricEvent::StreamReapedWindowStall);
5372 if matches!(self.stream_table.fc_stall_progress(sid), Some(acc) if acc > 0) {
5373 self.metric_events
5374 .push(MetricEvent::StreamReapedStallBudget);
5375 }
5376 }
5377 "H2::IdleTimeout" => self
5378 .metric_events
5379 .push(MetricEvent::StreamReapedIdleTimeout),
5380 other => debug!("{} unexpected reap reason {}", log_context!(self), other),
5381 }
5382 // Route through the canonical chokepoint so dedupe (rst_sent), the
5383 // pending-queue bound, and edge-triggered-epoll arming
5384 // (Readiness::arm_writable) all stay consistent — see LIFECYCLE
5385 // §8.2. The previous direct push bypassed all three: a mass reap
5386 // could push past the queue bound silently, a double-cancel pass
5387 // would grow the pending queue instead of short-circuiting on the
5388 // existing rst_sent membership, and the hand-rolled
5389 // `interest.insert(WRITABLE) + signal_pending_write` pair skipped
5390 // invariant 15.
5391 //
5392 // The CANCEL is Sōzu's own decision (`RstOrigin::Local`), so it is
5393 // not charged to the peer's MadeYouReset count: an idle stream is
5394 // also what a slow backend, a long poll or a paused download looks
5395 // like, and an idle stream pins one MAX_CONCURRENT_STREAMS slot for
5396 // the idle timeout without amplifying anything.
5397 //
5398 // We deliberately ignore the `Option<MuxResult>` signal here —
5399 // `cancel_timed_out_streams` returns `()` and is called as
5400 // best-effort housekeeping during the read path. A local reset
5401 // never trips the flood detector, and a queue overflow is
5402 // escalated by the next `flush_pending_control_frames`.
5403 //
5404 // A backend stream whose HEADERS never left is forgotten, not
5405 // reset: a RST_STREAM on a stream the backend has never seen is a
5406 // connection error that takes down every stream of the shared
5407 // connection (sozu-proxy/sozu#1631, see the `Position::Client` arm
5408 // of `ConnectionH2::end_stream`).
5409 let never_opened = matches!(self.position, Position::Client(..))
5410 && self
5411 .stream_table
5412 .get(sid)
5413 .is_some_and(|gid| !context.streams[gid].front.consumed);
5414 let queued_before = self.control_tx.lifetime_queued();
5415 if !never_opened {
5416 let _ = self.enqueue_rst(sid, H2Error::Cancel, RstOrigin::Local);
5417 }
5418 debug_assert!(
5419 !never_opened || self.control_tx.lifetime_queued() == queued_before,
5420 "a backend stream reaped before its HEADERS left must not be reset"
5421 );
5422
5423 // Remove from streams map and recycle the context stream so the slot
5424 // no longer counts against MAX_CONCURRENT_STREAMS.
5425 // Compute totals per-stream before remove (matches RST_STREAM handler).
5426 let byte_totals = self.compute_stream_byte_totals(context);
5427 if let Some(global_stream_id) = self.stream_table.get(sid) {
5428 {
5429 let stream = &mut context.streams[global_stream_id];
5430 self.attribute_bytes_to_stream(&mut stream.metrics);
5431 }
5432 // Check if stream is linked to a backend — borrow must be scoped
5433 // so end_stream can take &mut context.
5434 let linked_token = context.streams[global_stream_id].linked_token();
5435 let (client_rtt, server_rtt) = self.snapshot_rtts(&*endpoint, linked_token);
5436 if let Some(token) = linked_token {
5437 endpoint.end_stream(token, global_stream_id, context);
5438 }
5439 let stream = &mut context.streams[global_stream_id];
5440 match &self.position {
5441 Position::Client(_, backend, BackendStatus::Connected) => {
5442 context.record_backend_delta(backend, BackendChange::StreamsEnded(1));
5443 }
5444 Position::Client(..) => {}
5445 Position::Server => {
5446 self.distribute_overhead(&mut stream.metrics, byte_totals);
5447 stream.metrics.backend_stop();
5448 let events = stream.generate_access_log(
5449 true,
5450 Some(reason),
5451 context.listener.clone(),
5452 client_rtt,
5453 server_rtt,
5454 );
5455 self.metric_events.extend(events.into_iter().flatten());
5456 stream.state = StreamState::Recycle;
5457 }
5458 }
5459 // Retire sid from streams/prioriser/stream_last_activity_at and
5460 // invalidate expect_write/expect_read if they reference this gid.
5461 self.remove_dead_stream(sid, global_stream_id);
5462 }
5463 }
5464 // Writable arming is already done by enqueue_rst -> arm_writable in
5465 // the loop above; the trailing pair was redundant after the chokepoint
5466 // routing landed.
5467 }
5468
5469 /// Queue a `RST_STREAM` frame for serialisation by
5470 /// [`Self::flush_pending_control_frames`] on the next writable tick.
5471 ///
5472 /// This is the canonical entry point for proxy-emitted stream resets:
5473 /// `DATA` on a closed stream, `MAX_CONCURRENT_STREAMS` refusal, and the
5474 /// per-stream error paths in [`Self::reset_stream`] all funnel through
5475 /// here. `origin` says whether the peer provoked the reset; see
5476 /// [`RstOrigin`]. Serialisation is independent of the owning `Stream` still
5477 /// existing in `self.streams`, which is what lets us emit even after a
5478 /// caller has already called [`Self::remove_dead_stream`].
5479 ///
5480 /// Delegates the queueing itself to [`h2_control_tx::H2ControlTx::enqueue_rst`],
5481 /// which owns the invariants (dedupe via `rst_sent`, the per-insert queue
5482 /// bound and its overflow flag, edge-triggered-epoll arm via
5483 /// [`Readiness::arm_writable`]) and covers them with unit tests that need no
5484 /// `ConnectionH2` fixture. What stays here is the accounting, because a
5485 /// MadeYouReset cap trip converts to a connection-wide GOAWAY only this
5486 /// type can return.
5487 ///
5488 /// Two of the call paths that reach here are never inspected by
5489 /// [`Self::check_invariants`], whose only production caller is
5490 /// [`Self::handle_frame`]:
5491 ///
5492 /// * `cancel_timed_out_streams`, which queues one RST per timed-out stream
5493 /// in a single sweep and, from `Mux::timeout` against a silent peer,
5494 /// runs when `handle_frame` does not run at all;
5495 /// * the DATA-on-closed-stream reset, which sits in
5496 /// [`Self::handle_header_state`] — `handle_read` returns into that helper
5497 /// directly and that branch never reaches `handle_frame`.
5498 ///
5499 /// The second path is separately rate-limited: it is preceded by
5500 /// `H2FloodDetector::record_glitch` + `check_flood_or_return!`, whose
5501 /// `DEFAULT_MAX_GLITCH_COUNT` (2000) bounds it inside one flood window, and
5502 /// the window can only half-decay when `Mux::ready_inner` resamples
5503 /// `context.now` on a new outer iteration — which is strictly after an
5504 /// inner iteration that already ran `writable()`, since `arm_writable`
5505 /// raises both the WRITABLE interest and its event bit. So it cannot
5506 /// accumulate across windows without a `flush_pending_control_frames`
5507 /// pass in between. The per-insert bound inside
5508 /// [`h2_control_tx::H2ControlTx::enqueue_rst`] is what makes that reasoning
5509 /// unnecessary for correctness.
5510 fn enqueue_rst(
5511 &mut self,
5512 wire_stream_id: StreamId,
5513 error: H2Error,
5514 origin: RstOrigin,
5515 ) -> Option<MuxResult> {
5516 // Only a registered stream's id goes into `rst_sent`: eviction from
5517 // the stream table is what removes it again. A refused stream or a
5518 // closed one was never (or is no longer) registered, and its id would
5519 // otherwise stay in the set for the connection's lifetime. A refusal
5520 // records its id in the bounded recently-reset set instead
5521 // (`H2StreamTable::remember_refused`).
5522 let registered = self.stream_table.get(wire_stream_id).is_some();
5523 let rst_sent = registered.then(|| self.stream_table.rst_sent_mut());
5524 let outcome =
5525 self.control_tx
5526 .enqueue_rst(rst_sent, &mut self.readiness, wire_stream_id, error);
5527 // Account ONLY when a new RST actually entered the queue.
5528 // Calling `enqueue_rst` for a stream that already has a queued
5529 // (or already-flushed) RST is the dedup short-circuit — counting
5530 // those would inflate `h2.frames.tx.rst_stream` /
5531 // `h2.rst_stream.sent.*` and the CVE-2025-8671 MadeYouReset
5532 // count on frames that never reached the wire. An
5533 // at-capacity refusal is accounted the same way, and for the same
5534 // reason: that frame never reaches the wire either.
5535 //
5536 // Account at queue-time, not at drain-time. Doing it later in
5537 // `flush_pending_control_frames` would double-count any RST that
5538 // a re-entrant call (DATA on a closed stream we already RSTed)
5539 // tried to enqueue — and missing it at queue-time leaves
5540 // `cancel_timed_out_streams` / `refuse_stream_and_discard` /
5541 // DATA-on-closed-stream paths bypassing the lifetime cap
5542 // (security review LISA-001 on commit `da845c71`).
5543 match outcome {
5544 h2_control_tx::EnqueueRstOutcome::Queued => self.account_emitted_rst(error, origin),
5545 h2_control_tx::EnqueueRstOutcome::Deduped => None,
5546 // Drop + metric + contextual log, never a panic on the release
5547 // path. No GOAWAY is raised here: the refusal set
5548 // `H2ControlTx::overflowed`, which
5549 // `flush_pending_control_frames` escalates to
5550 // `GOAWAY(ENHANCE_YOUR_CALM)` before its drain loop for as long as
5551 // the connection is not already in `H2State::GoAway`/`Error` — and
5552 // once it is, the peer already holds a GOAWAY and `writable()`
5553 // force-disconnects. See `EnqueueRstOutcome::Dropped`. Raising a
5554 // second one from here would re-enter `goaway()` once per
5555 // remaining reaped stream, clobbering `self.zero` and inflating
5556 // `h2.goaway.sent.*`.
5557 h2_control_tx::EnqueueRstOutcome::Dropped => {
5558 error!(
5559 "{} RST_STREAM dropped: pending queue already at capacity ({}), stream={} error={:?}",
5560 log_context!(self),
5561 self.control_tx.max_pending(),
5562 wire_stream_id,
5563 error
5564 );
5565 self.metric_events.push(MetricEvent::RstStreamDropped);
5566 None
5567 }
5568 }
5569 }
5570
5571 /// Single accounting site for proxy-emitted RST_STREAM frames.
5572 /// Every emitted RST bumps the global tx counter and the per-error
5573 /// breakdown; only a non-`NoError` reset the peer provoked
5574 /// ([`RstOrigin::PeerProvoked`]) also feeds the MadeYouReset emitted-RST
5575 /// cap.
5576 ///
5577 /// Two distinct emission paths feed this helper:
5578 /// * Queued frames — `Self::enqueue_rst` (and therefore every
5579 /// callable that funnels through it: `reset_stream`,
5580 /// `refuse_stream_and_discard`, `cancel_timed_out_streams`,
5581 /// DATA-on-closed-stream) calls this once at queue-time. The
5582 /// drain in `flush_pending_control_frames` does NOT call it
5583 /// again — that would double-count.
5584 /// * Converter-emitted frames — the converter's `initialize`
5585 /// chokepoint (and the HPACK over-budget abort path) writes
5586 /// RST_STREAM frames straight into `kawa.out` from inside
5587 /// `kawa.prepare`. We collect those `H2Error` codes during the
5588 /// `write_streams` loop and call this helper for each one
5589 /// after the loop. They are all [`RstOrigin::Local`]: what errored
5590 /// is the message coming from the other side of the proxy — a
5591 /// backend response on a frontend connection, a client request on a
5592 /// backend one — never a frame this connection's peer sent.
5593 ///
5594 /// Returning `Some(MuxResult)` means the caller MUST short-circuit
5595 /// with that result — the flood detector tripped its lifetime cap
5596 /// and converted to a connection-wide GOAWAY.
5597 fn account_emitted_rst(&mut self, error: H2Error, origin: RstOrigin) -> Option<MuxResult> {
5598 self.metric_events.push(MetricEvent::RstStreamSent(error));
5599 if origin == RstOrigin::PeerProvoked && !matches!(error, H2Error::NoError) {
5600 if let Some(violation) = self.flood_detector.record_rst_emitted() {
5601 return Some(self.handle_flood_violation(violation));
5602 }
5603 // A provoked reset answers a protocol error by the peer, so it is
5604 // also a glitch: the per-window rate limit the ratio cap above
5605 // lacks. None of the `PeerProvoked` call sites records a glitch
5606 // of its own, so this is the only charge.
5607 self.flood_detector.record_glitch();
5608 if let Some(violation) = self.flood_detector.check_flood(self.now) {
5609 return Some(self.handle_flood_violation(violation));
5610 }
5611 }
5612 None
5613 }
5614
5615 /// Refuse a newly-opened stream with RST_STREAM and discard its HEADERS payload.
5616 ///
5617 /// Used when MAX_CONCURRENT_STREAMS is exceeded or buffer pool is exhausted.
5618 /// Queues the RST_STREAM for the writable path (can't write to kawa.storage
5619 /// here because it is needed to discard the HEADERS payload).
5620 ///
5621 /// Also applies SETTINGS back-pressure per RFC 9113 §5.1.2: if refusals
5622 /// burst past [`BACKPRESSURE_REFUSAL_THRESHOLD`] within
5623 /// [`BACKPRESSURE_WINDOW_DURATION`], the advertised
5624 /// `SETTINGS_MAX_CONCURRENT_STREAMS` is halved via
5625 /// [`Self::apply_mcs_backpressure`].
5626 ///
5627 /// `discarded` is stashed in [`Self::discarded_field_block`] for the
5628 /// `H2State::Discard` arm of [`Self::handle_read`] to consume — see
5629 /// [`DiscardedFieldBlock`] for why the HPACK field block cannot simply be
5630 /// dropped with the rest of the payload.
5631 fn refuse_stream_and_discard(
5632 &mut self,
5633 stream_id: StreamId,
5634 error: H2Error,
5635 origin: RstOrigin,
5636 payload_len: u32,
5637 discarded: DiscardedFieldBlock,
5638 ) -> MuxResult {
5639 if let Some(result) = self.enqueue_rst(stream_id, error, origin) {
5640 return result;
5641 }
5642 // RFC 9113 §5.1: the frames the peer sent on this stream before it
5643 // read the refusal are ignored, as on any stream Sōzu reset.
5644 self.stream_table.remember_refused(stream_id);
5645 let result = self.discard_field_block(stream_id, payload_len, discarded);
5646 self.record_refusal_for_backpressure();
5647 result
5648 }
5649
5650 /// Extend [`Self::flood_refused_streams`] with `stream_id`, refused under
5651 /// flood pressure, or start a new run when a stream was accepted since
5652 /// the last refusal (the even watermark `last_stream_id` moved past the
5653 /// run's first id).
5654 fn note_flood_refused_stream(&mut self, stream_id: StreamId) {
5655 let run = match self.flood_refused_streams {
5656 Some((first, _)) if self.last_stream_id < first => (first, stream_id),
5657 _ => (stream_id, stream_id),
5658 };
5659 debug_assert!(
5660 run.0 <= run.1 && run.1 == stream_id,
5661 "the refused run ends at the stream just refused"
5662 );
5663 debug_assert!(
5664 self.last_stream_id < run.0,
5665 "no accepted stream lies inside the refused run"
5666 );
5667 self.flood_refused_streams = Some(run);
5668 }
5669
5670 /// Read the `payload_len` bytes of a HEADERS or CONTINUATION frame of
5671 /// `stream_id` that is dropped through `H2State::Discard`, which decodes
5672 /// its field block only to keep the HPACK dynamic table in sync (RFC
5673 /// 9113 §4.3), then resumes normal reading. Queues nothing. A frame
5674 /// without END_HEADERS leaves its fragment in
5675 /// [`Self::pending_discarded_block`], so the CONTINUATION frames that
5676 /// complete the block are discarded and decoded with it.
5677 fn discard_field_block(
5678 &mut self,
5679 stream_id: StreamId,
5680 payload_len: u32,
5681 discarded: DiscardedFieldBlock,
5682 ) -> MuxResult {
5683 // A new block's first frame counts toward the block's size like an
5684 // accepted one's (CVE-2024-27316), so its CONTINUATION frames are
5685 // held to the same `max_header_list_size`; the whole frame payload
5686 // stands in for the fragment, which it bounds.
5687 if matches!(discarded, DiscardedFieldBlock::New { .. }) {
5688 self.flood_detector.begin_header_block_if_new(payload_len);
5689 }
5690 self.state = H2State::Discard;
5691 self.stream_table
5692 .set_expect_read(Some((H2StreamId::Zero, payload_len as usize)));
5693 self.discarded_field_block = Some((stream_id, discarded));
5694 MuxResult::Continue
5695 }
5696
5697 /// Minimally process and drop the payload of the DATA or HEADERS frame
5698 /// whose `header` was just parsed, for a stream that is reset or being
5699 /// reset (RFC 9113 §5.1): a field block, with the CONTINUATION frames
5700 /// completing it, still updates the HPACK decoder, DATA still counts
5701 /// toward the connection flow-control window.
5702 fn discard_frame_payload(&mut self, header: &FrameHeader) -> MuxResult {
5703 debug_assert!(
5704 matches!(header.frame_type, FrameType::Data | FrameType::Headers),
5705 "only DATA and HEADERS payloads are discarded per stream"
5706 );
5707 if header.frame_type == FrameType::Headers {
5708 self.discard_field_block(
5709 header.stream_id,
5710 header.payload_len,
5711 DiscardedFieldBlock::New {
5712 flags: header.flags,
5713 },
5714 )
5715 } else {
5716 self.skip_orphaned_data_payload(header.payload_len as usize, header.payload_len);
5717 MuxResult::Continue
5718 }
5719 }
5720
5721 /// RFC 9113 §5.4.2: answer a stream error on the open stream `header`
5722 /// names with RST_STREAM(`error`), tear the stream down, and discard the
5723 /// frame's payload ([`Self::discard_frame_payload`]). Every other stream
5724 /// on the connection carries on. The RST_STREAM goes through
5725 /// [`Self::reset_stream`], so it feeds the emitted-RST flood accounting.
5726 fn reset_stream_and_discard_frame<E, L>(
5727 &mut self,
5728 header: &FrameHeader,
5729 global_stream_id: GlobalStreamId,
5730 error: H2Error,
5731 context: &mut Context<L>,
5732 endpoint: E,
5733 ) -> MuxResult
5734 where
5735 E: Endpoint,
5736 L: ListenerHandler + L7ListenerHandler,
5737 {
5738 let result =
5739 self.reset_stream(header.stream_id, global_stream_id, context, endpoint, error);
5740 self.remove_dead_stream(header.stream_id, global_stream_id);
5741 if !matches!(result, MuxResult::Continue) {
5742 return result;
5743 }
5744 self.discard_frame_payload(header)
5745 }
5746
5747 /// RFC 9113 §5.1.2 SETTINGS back-pressure bookkeeping.
5748 ///
5749 /// Increments the refusal counter for the current back-pressure window
5750 /// and, when the burst threshold is crossed, halves the advertised
5751 /// `SETTINGS_MAX_CONCURRENT_STREAMS`. Further halving attempts in the
5752 /// same connection are suppressed by [`Self::mcs_backpressure_applied`]
5753 /// so sustained abuse does not collapse the cap to zero — callers can
5754 /// still promote the situation to `EnhanceYourCalm` via the flood
5755 /// detector.
5756 fn record_refusal_for_backpressure(&mut self) {
5757 if self.now.saturating_duration_since(self.refuse_window_start)
5758 >= BACKPRESSURE_WINDOW_DURATION
5759 {
5760 self.refuse_count_window = 0;
5761 self.refuse_window_start = self.now;
5762 }
5763 self.refuse_count_window = self.refuse_count_window.saturating_add(1);
5764 if !self.mcs_backpressure_applied
5765 && self.refuse_count_window >= BACKPRESSURE_REFUSAL_THRESHOLD
5766 {
5767 self.apply_mcs_backpressure();
5768 }
5769 }
5770
5771 /// Halve the advertised `SETTINGS_MAX_CONCURRENT_STREAMS` and mark the
5772 /// back-pressure state as applied. The new value takes effect locally
5773 /// immediately — subsequent stream-open checks in `handle_header_state`
5774 /// compare `self.stream_table.streams().len()` against this reduced cap, so the peer
5775 /// starts receiving `REFUSED_STREAM` earlier. A full SETTINGS re-send on
5776 /// the wire is deferred until we have a mid-connection SETTINGS queue
5777 /// (the existing path in `handle_preface_state` only fires during the
5778 /// handshake); this is noted in the task log as a minimal first step.
5779 fn apply_mcs_backpressure(&mut self) {
5780 let previous = self.local_settings.settings_max_concurrent_streams;
5781 let reduced = (previous / 2).max(1);
5782 warn!(
5783 "{} H2 SETTINGS back-pressure: refusals={} in {}s — halving \
5784 SETTINGS_MAX_CONCURRENT_STREAMS {} -> {}",
5785 log_context!(self),
5786 self.refuse_count_window,
5787 BACKPRESSURE_WINDOW_DURATION.as_secs(),
5788 previous,
5789 reduced,
5790 );
5791 self.local_settings.settings_max_concurrent_streams = reduced;
5792 self.mcs_backpressure_applied = true;
5793 }
5794
5795 /// Log a flood violation with full session context and emit the GOAWAY.
5796 ///
5797 /// Centralises the "flood detected" reporting so every site that observes a
5798 /// `H2FloodViolation` gets the same session-scoped log line, matching the
5799 /// RUSTLS log-context convention. Also emits the per-kind statsd counter
5800 /// (`h2.flood.violation.<kind>`) so SOC dashboards can window the trip
5801 /// rate without parsing logs — every CVE-mitigation in the H2 family
5802 /// (Rapid Reset, MadeYouReset, CONTINUATION/PING/SETTINGS floods, header
5803 /// overflow, glitch) funnels through this site.
5804 pub fn handle_flood_violation(&mut self, violation: H2FloodViolation) -> MuxResult {
5805 self.metric_events.push(MetricEvent::FloodViolation {
5806 metric_key: violation.metric_key,
5807 });
5808 warn!(
5809 "{} H2 flood detected: {} count {} exceeds threshold {}",
5810 log_context!(self),
5811 violation.reason,
5812 violation.count,
5813 violation.threshold,
5814 );
5815 self.goaway(violation.error)
5816 }
5817}
5818
5819/// Whether an answered frontend stream counts toward the RST caps'
5820/// denominator (`H2FloodDetector`'s `streams_opened`,
5821/// `lib/src/protocol/mux/h2_flood_detector.rs`): it was routed to a backend.
5822/// A backend answered it, a backend was selected for it — including when
5823/// Sōzu then answered 502/503/504 because that backend refused the
5824/// connection, failed or timed out — or its cluster has backends and none
5825/// could be selected because all of them are failing (`backends_unavailable`).
5826/// So a backend outage does not turn the client's ordinary cancels into a
5827/// Rapid Reset verdict. A stream answered before any backend was selected
5828/// for another reason does not count, whatever its status: no route, a
5829/// redirect, 401, 421, 429, a refusal, a 503 for a cluster with no backend,
5830/// or a session or buffer limit hit before selection. The peer opens those
5831/// for free, and counting them would let filler streams dilute the ratio.
5832fn routed_to_a_backend(stream: &crate::protocol::mux::Stream) -> bool {
5833 stream.metrics.backend_headers_received.is_some()
5834 || stream.context.backend_id.is_some()
5835 || stream.context.backends_unavailable
5836}
5837
5838/// Who decided a proxy-emitted `RST_STREAM`, for flood accounting.
5839///
5840/// Only a reset the peer provoked counts toward the CVE-2025-8671
5841/// MadeYouReset cap (`H2FloodDetector::record_rst_emitted`,
5842/// `lib/src/protocol/mux/h2_flood_detector.rs`). A reset Sōzu decides on its
5843/// own — the idle reaper's `CANCEL`, a `REFUSED_STREAM` from its own
5844/// concurrency limit, back-pressure, buffer pool or flood soft state, `STREAM_CLOSED` for DATA
5845/// on a stream it already closed, the converter's error on a backend failure —
5846/// says nothing about the peer, and counting it would end healthy connections
5847/// for Sōzu's own decisions.
5848#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5849enum RstOrigin {
5850 /// A frame the peer sent forced the reset: Content-Length mismatch,
5851 /// header parse error, oversized header block, PRIORITY
5852 /// self-dependency, zero-increment or overflowing WINDOW_UPDATE.
5853 PeerProvoked,
5854 /// Sōzu reset the stream on its own initiative.
5855 Local,
5856}
5857
5858/// Recover the [`H2Error`] code that the converter's `initialize`
5859/// chokepoint will encode into the synthesised RST_STREAM frame for a
5860/// kawa stuck in [`kawa::ParsingPhase::Error`]. Mirrors the parse +
5861/// fallback at `lib/src/protocol/mux/converter.rs::initialize` so the
5862/// flood-accounting helper sees the same code that lands on the wire.
5863fn rst_error_from_kawa<T: kawa::AsBuffer>(kawa: &kawa::Kawa<T>) -> H2Error {
5864 match kawa.parsing_phase {
5865 kawa::ParsingPhase::Error {
5866 kind: kawa::ParsingErrorKind::Processing { message },
5867 ..
5868 } => message.parse::<H2Error>().unwrap_or(H2Error::InternalError),
5869 _ => H2Error::InternalError,
5870 }
5871}
5872
5873/// Compile-time mapping from `(prefix, H2Error)` to a static metric key.
5874///
5875/// Materialises a `&'static str` literal via `concat!`, so the metric key
5876/// never crosses through a heap allocation and the statsd drain can store it
5877/// as `&'static str`. Adding a new `H2Error` variant fails the build here —
5878/// the metric breakdown stays in lock-step with RFC 9113 §7 codes.
5879///
5880/// Used for the per-error-code counters emitted around GOAWAY and RST_STREAM
5881/// in either direction (see `metric_for_goaway_sent` etc. below).
5882macro_rules! h2_error_metric_key {
5883 ($prefix:literal, $error:expr) => {
5884 match $error {
5885 H2Error::NoError => concat!($prefix, ".no_error"),
5886 H2Error::ProtocolError => concat!($prefix, ".protocol_error"),
5887 H2Error::InternalError => concat!($prefix, ".internal_error"),
5888 H2Error::FlowControlError => concat!($prefix, ".flow_control_error"),
5889 H2Error::SettingsTimeout => concat!($prefix, ".settings_timeout"),
5890 H2Error::StreamClosed => concat!($prefix, ".stream_closed"),
5891 H2Error::FrameSizeError => concat!($prefix, ".frame_size_error"),
5892 H2Error::RefusedStream => concat!($prefix, ".refused_stream"),
5893 H2Error::Cancel => concat!($prefix, ".cancel"),
5894 H2Error::CompressionError => concat!($prefix, ".compression_error"),
5895 H2Error::ConnectError => concat!($prefix, ".connect_error"),
5896 H2Error::EnhanceYourCalm => concat!($prefix, ".enhance_your_calm"),
5897 H2Error::InadequateSecurity => concat!($prefix, ".inadequate_security"),
5898 H2Error::HTTP11Required => concat!($prefix, ".http_1_1_required"),
5899 }
5900 };
5901}
5902
5903/// Static metric key for an outbound GOAWAY. Same call shape as the other three
5904/// helpers below — keeps the call sites uniform.
5905fn metric_for_goaway_sent(error: H2Error) -> &'static str {
5906 h2_error_metric_key!("h2.goaway.sent", error)
5907}
5908
5909/// Static metric key for an inbound GOAWAY by raw wire error code. Codes
5910/// outside RFC 9113 §7 fall into the dedicated `…unknown_error` bucket so the
5911/// breakdown stays bounded and operators can still spot non-standard peers.
5912fn metric_for_goaway_received(error_code: u32) -> &'static str {
5913 H2Error::try_from(error_code)
5914 .map(|e| h2_error_metric_key!("h2.goaway.received", e))
5915 .unwrap_or("h2.goaway.received.unknown_error")
5916}
5917
5918/// Static metric key for an outbound RST_STREAM. Mirrors
5919/// [`metric_for_goaway_sent`] under a separate namespace so RST and GOAWAY
5920/// rates can be alerted on independently.
5921fn metric_for_rst_stream_sent(error: H2Error) -> &'static str {
5922 h2_error_metric_key!("h2.rst_stream.sent", error)
5923}
5924
5925/// Static metric key for an inbound RST_STREAM by raw wire error code. Same
5926/// `…unknown_error` fallback as [`metric_for_goaway_received`].
5927fn metric_for_rst_stream_received(error_code: u32) -> &'static str {
5928 H2Error::try_from(error_code)
5929 .map(|e| h2_error_metric_key!("h2.rst_stream.received", e))
5930 .unwrap_or("h2.rst_stream.received.unknown_error")
5931}
5932
5933/// Static metric key for an inbound H2 frame by RFC 9113 §6 frame type.
5934/// Emitted at the `handle_frame` dispatch — single chokepoint that any
5935/// new H2 frame type must traverse, so adding a `Frame::*` variant fails
5936/// the build here. Counts are per-frame, not per-byte; pair with
5937/// `bytes_in` for traffic-mix dashboards.
5938fn h2_frame_rx_metric_key(frame_type: FrameType) -> &'static str {
5939 match frame_type {
5940 FrameType::Data => "h2.frames.rx.data",
5941 FrameType::Headers => "h2.frames.rx.headers",
5942 FrameType::PushPromise => "h2.frames.rx.push_promise",
5943 FrameType::Priority => "h2.frames.rx.priority",
5944 FrameType::RstStream => "h2.frames.rx.rst_stream",
5945 FrameType::Settings => "h2.frames.rx.settings",
5946 FrameType::Ping => "h2.frames.rx.ping",
5947 FrameType::GoAway => "h2.frames.rx.goaway",
5948 FrameType::WindowUpdate => "h2.frames.rx.window_update",
5949 FrameType::Continuation => "h2.frames.rx.continuation",
5950 FrameType::PriorityUpdate => "h2.frames.rx.priority_update",
5951 FrameType::Unknown(_) => "h2.frames.rx.unknown",
5952 }
5953}
5954
5955impl ConnectionH2 {
5956 /// Queue the final GOAWAY with `last_stream_id` set to the highest
5957 /// peer-initiated stream observed (`H2StreamTable::highest_peer_stream_id`).
5958 pub fn goaway(&mut self, error: H2Error) -> MuxResult {
5959 let last_stream_id = self.stream_table.highest_peer_stream_id();
5960 self.goaway_with_last_stream_id(error, last_stream_id)
5961 }
5962
5963 /// True while a peer HEADERS block is still being reassembled
5964 /// (`H2State::ContinuationHeader` / `H2State::ContinuationFrame`).
5965 pub fn peer_header_block_in_progress(&self) -> bool {
5966 matches!(
5967 self.state,
5968 H2State::ContinuationHeader(_) | H2State::ContinuationFrame(_)
5969 )
5970 }
5971
5972 /// RFC 9113 §6.8: the final GOAWAY a draining connection sends when its
5973 /// graceful-shutdown budget elapses, just before `Mux::shutting_down_inner`
5974 /// (`lib/src/protocol/mux/mod.rs`) closes it. Without it a forced close
5975 /// leaves the peer holding only the advisory `2^31-1` GOAWAY, which says
5976 /// nothing about which streams were processed (sozu-proxy/sozu#1647).
5977 ///
5978 /// A stream whose opening field block is still being reassembled is
5979 /// excluded: it is `StreamState::Idle`, it never reached a backend, and
5980 /// the peer may retry it. `create_stream` already counted it in
5981 /// `H2StreamTable::highest_peer_stream_id` when its HEADERS frame header
5982 /// arrived. Every stream `create_stream` accepted before it has a lower
5983 /// id (RFC 9113 §5.1.1, enforced by `handle_header_state` against
5984 /// `Self::last_stream_id`), and none can be accepted after it while the
5985 /// block is open (§6.10), so every stream sozu may have acted on is below
5986 /// it: the reported id is the one two below it (the previous odd id, or
5987 /// 0). A higher id counted in `highest_peer_stream_id` can only be a
5988 /// refused stream, which sozu did not act on either.
5989 ///
5990 /// A connection already in `H2State::GoAway` or `H2State::Error` has
5991 /// sent (or failed to send) its final GOAWAY and gets no second one.
5992 pub fn goaway_before_forced_close<L>(&mut self, context: &Context<L>) -> MuxResult
5993 where
5994 L: ListenerHandler + L7ListenerHandler,
5995 {
5996 debug_assert!(
5997 self.drain.draining(),
5998 "a forced close follows a graceful drain, which marks the connection draining"
5999 );
6000 if matches!(self.state, H2State::GoAway | H2State::Error) {
6001 return MuxResult::Continue;
6002 }
6003 let highest = self.stream_table.highest_peer_stream_id();
6004 let last_stream_id = match &self.state {
6005 H2State::ContinuationHeader(headers) | H2State::ContinuationFrame(headers)
6006 if self
6007 .stream_table
6008 .get(headers.stream_id)
6009 .and_then(|global_stream_id| context.streams.get(global_stream_id))
6010 .is_some_and(|stream| stream.state == StreamState::Idle) =>
6011 {
6012 headers.stream_id.saturating_sub(2)
6013 }
6014 _ => highest,
6015 };
6016 debug_assert!(
6017 last_stream_id <= highest && (last_stream_id == 0 || last_stream_id & 1 == 1),
6018 "the final GOAWAY reports 0 or a peer-initiated id no higher than any observed"
6019 );
6020 self.goaway_with_last_stream_id(H2Error::NoError, last_stream_id)
6021 }
6022
6023 fn goaway_with_last_stream_id(
6024 &mut self,
6025 error: H2Error,
6026 last_stream_id: StreamId,
6027 ) -> MuxResult {
6028 self.state = H2State::Error;
6029 self.drain.enter_final_goaway();
6030 self.graceful_goaway = error == H2Error::NoError;
6031 self.stream_table.set_expect_read(None);
6032 // Disarm the SETTINGS ACK timer: once we've committed to GOAWAY, the
6033 // timeout check at `readable()` / `flush_pending_control_frames()` must
6034 // not re-fire. Without this, `signal_pending_write()` below re-enters
6035 // `writable()` → `flush_pending_control_frames()` on the next tick,
6036 // the elapsed check is still true, and we emit another
6037 // `warn!` + `goaway()` pair, each bumping `h2.goaway.sent.*`.
6038 self.settings_sent_at = None;
6039 // Severity tiering: only `InternalError` implies a sozu-side bug when
6040 // WE emit it. Every other non-`NoError` reason is "peer misbehaved,
6041 // sozu defended correctly" — operators don't need paging on abusive
6042 // or buggy peers. Caller sites already log the specific antecedent
6043 // (flood detected, parser failure, SETTINGS timeout, invalid window)
6044 // before reaching `goaway()`, so demoting this summary line avoids
6045 // duplicate noise without hiding the root cause.
6046 match error {
6047 H2Error::NoError => debug!("{} GOAWAY: {:?}", log_context!(self), error),
6048 H2Error::InternalError => error!("{} GOAWAY: {:?}", log_context!(self), error),
6049 _ => warn!("{} GOAWAY: {:?}", log_context!(self), error),
6050 }
6051 self.metric_events.push(MetricEvent::GoAwaySent(error));
6052
6053 // RFC 9113 §6.8: last_stream_id is the highest peer-initiated stream
6054 // we processed. Queued behind whatever output is already waiting (an
6055 // ACK, the rest of a stream frame), never in place of it.
6056 match self.output.push_frames(GOAWAY_FRAME_SIZE, |buf| {
6057 serializer::gen_goaway(buf, last_stream_id, error).map(|(_, size)| size)
6058 }) {
6059 Ok(_) => {
6060 self.metric_events.push(MetricEvent::GoAwayFrameSent);
6061 self.state = H2State::GoAway;
6062 self.readiness.interest = Ready::WRITABLE | Ready::HUP | Ready::ERROR;
6063 self.readiness.signal_pending_write();
6064 MuxResult::Continue
6065 }
6066 Err(error) => {
6067 error!(
6068 "{} Could not serialize GoAwayFrame: {:?}",
6069 log_context!(self),
6070 error
6071 );
6072 self.force_disconnect()
6073 }
6074 }
6075 }
6076
6077 /// RFC 9113 §6.8: Initiate graceful shutdown using the double-GOAWAY pattern.
6078 ///
6079 /// First call sends GOAWAY with `last_stream_id = 0x7FFFFFFF` (MAX) to signal
6080 /// the intent to stop accepting new streams while allowing in-flight streams
6081 /// to complete. The connection enters draining mode.
6082 ///
6083 /// When `draining` is already true (second invocation), sends the final GOAWAY
6084 /// with the actual `highest_peer_stream_id` so the peer knows which streams
6085 /// were processed.
6086 ///
6087 /// `now` is the caller's clock snapshot and is what arms the forced-close
6088 /// budget. It is a parameter rather than a read of `Self::now` because
6089 /// the caller that matters — `Mux::shutting_down` — runs outside
6090 /// `ready()` and therefore outside the pass that last refreshed the
6091 /// mirror. In-module callers pass `self.now`.
6092 pub fn graceful_goaway(&mut self, now: Instant) -> MuxResult {
6093 // The initial GOAWAY goes out even while a peer HEADERS/CONTINUATION
6094 // block is still being reassembled: RFC 9113 §4.3/§6.10 constrain the
6095 // block's sender, not a GOAWAY travelling the other way, and it is
6096 // queued in `self.output`, which shares nothing with `zero` or
6097 // `header_reassembly`. Deferring it (#1401, removed in #1637) let a
6098 // drain close a session whose only stream was mid-block without any
6099 // GOAWAY — see `h2_drain`'s module doc.
6100 match self.drain.begin_graceful_drain(now) {
6101 // Second GOAWAY: send with the real last_stream_id.
6102 GracefulDrainDecision::AlreadyDraining => self.goaway(H2Error::NoError),
6103 // First GOAWAY: advertise MAX stream ID so the peer knows we are
6104 // draining but does not yet know the cutoff. This gives in-flight
6105 // requests a chance to arrive before we commit to a final
6106 // last_stream_id.
6107 GracefulDrainDecision::SendInitial => self.send_initial_goaway(),
6108 }
6109 }
6110
6111 /// Serializes and queues the first, advisory GOAWAY
6112 /// (`NO_ERROR`, `last_stream_id = STREAM_ID_MAX`) of a graceful drain
6113 /// in the ordered output queue (`self.output`), behind whatever is
6114 /// already queued there.
6115 fn send_initial_goaway(&mut self) -> MuxResult {
6116 // Keep expect_read as-is: existing streams should continue reading
6117 // data during the drain window opened by the initial GOAWAY. Only
6118 // the final GOAWAY (via `goaway()`) removes READABLE.
6119 debug!(
6120 "{} GOAWAY (graceful, initial): last_stream_id=0x7FFFFFFF",
6121 log_context!(self)
6122 );
6123 // The initial GOAWAY sends NO_ERROR on the wire — count it under
6124 // the same per-code key as the final GOAWAY. The downstream alert
6125 // that wants to distinguish drain from termination compares
6126 // against the `h2.goaway.sent.no_error` rate (drain) vs the other
6127 // variants (termination on error).
6128 self.metric_events
6129 .push(MetricEvent::GoAwaySent(H2Error::NoError));
6130
6131 // Queued behind whatever output is already waiting, like the final one.
6132 match self.output.push_frames(GOAWAY_FRAME_SIZE, |buf| {
6133 serializer::gen_goaway(buf, STREAM_ID_MAX, H2Error::NoError).map(|(_, size)| size)
6134 }) {
6135 Ok(_) => {
6136 self.metric_events.push(MetricEvent::GoAwayFrameSent);
6137 // Stay in the current state so the connection can continue processing
6138 // existing streams. The final GOAWAY will transition to GoAway state.
6139 // Keep READABLE so in-flight request bodies can still be received
6140 // during the drain window. Only remove READABLE in the final GOAWAY
6141 // (via `goaway()`).
6142 self.readiness.arm_writable();
6143 MuxResult::Continue
6144 }
6145 Err(error) => {
6146 error!(
6147 "{} Could not serialize graceful GoAwayFrame: {:?}",
6148 log_context!(self),
6149 error
6150 );
6151 self.force_disconnect()
6152 }
6153 }
6154 }
6155
6156 /// Returns `true` when the graceful-shutdown budget armed by
6157 /// [`Self::graceful_goaway`] has elapsed. A return of `true` signals
6158 /// the enclosing session loop that the proxy-initiated drain must
6159 /// transition to a forced close: remaining streams will not complete
6160 /// in time and keeping the connection open past the deadline defeats
6161 /// the soft-stop SLA.
6162 ///
6163 /// Returns `false` when:
6164 /// - drain has not started yet (`started_at` is `None`),
6165 /// - the knob is `0` / `None` (indefinite wait explicitly opted in),
6166 /// - or the elapsed time is still within the configured budget.
6167 pub fn graceful_shutdown_deadline_elapsed(&self) -> bool {
6168 self.drain.deadline_elapsed(self.now)
6169 }
6170
6171 /// Arm the graceful-shutdown budget of a connection a proxy soft-stop
6172 /// finds already draining, which `graceful_goaway` is not called for: a
6173 /// peer GOAWAY drains without arming it. See
6174 /// `H2DrainState::arm_deadline_if_unarmed` (`h2_drain.rs`).
6175 pub fn arm_graceful_shutdown_deadline(&mut self, now: Instant) {
6176 self.drain.arm_deadline_if_unarmed(now);
6177 }
6178
6179 /// True when the reaper has queued control frames (`RST_STREAM`) into
6180 /// `h2_control_tx::H2ControlTx` that have not yet been serialized. Kept SEPARATE
6181 /// from [`H2Shell::has_pending_write`] because that probe gates connection close
6182 /// (the `mod.rs` close-gating sites) and must NOT treat a queued RST as a
6183 /// reason to keep the connection open; this probe is consulted ONLY by the
6184 /// `MuxState::timeout` flush gate to push a silent-peer `RST_STREAM(CANCEL)`
6185 /// onto the wire before the connection closes.
6186 /// Adopt the embedder's clock snapshot for this pass.
6187 ///
6188 /// `Self::now` is mirrored from `Context::now` at each public entry
6189 /// point rather than read from the host — invariant 20 — and a driver
6190 /// standing where `H2Shell::writable` stands has to perform that mirror
6191 /// itself. The other entry points that need it do it inside their own
6192 /// bodies; the write pass is assembled by its caller, so its mirror is
6193 /// exposed.
6194 pub fn adopt_now(&mut self, now: Instant) {
6195 self.now = now;
6196 }
6197
6198 /// The landing zone an [`H2ReadTarget::Fill`] names.
6199 ///
6200 /// The read half of the byte-in / byte-out surface: a driver answers
6201 /// `Fill` by filling this slice and reporting what it managed, and never
6202 /// needs to know which buffer it belongs to. `H2Shell::readable` fills it
6203 /// from a socket; `sim/tests/h2_simulation.rs` fills it from a
6204 /// `VecDeque<u8>`, and that is the whole difference between them.
6205 ///
6206 /// Takes `context` apart from `self` rather than borrowing all of `*self`,
6207 /// for the reason `read_space` gives: the caller must keep the rest of
6208 /// the connection borrowable while the slice is live.
6209 pub fn read_space<'a, L>(
6210 &'a mut self,
6211 context: &'a mut Context<L>,
6212 stream_id: H2StreamId,
6213 amount: usize,
6214 ) -> &'a mut [u8]
6215 where
6216 L: ListenerHandler + L7ListenerHandler,
6217 {
6218 read_space(
6219 &mut self.zero,
6220 &mut context.streams,
6221 &self.position,
6222 stream_id,
6223 amount,
6224 )
6225 }
6226
6227 /// Gather one [`H2WriteTarget::Transmit`]: the ordered output queue
6228 /// first, then `stream_id`'s queued frames, through
6229 /// [`h2_transmit::gather_after`]. Answers `(prefix_len, bytes_offered)`.
6230 ///
6231 /// # Safety
6232 ///
6233 /// [`h2_transmit::gather_after`]'s contract: neither this connection's
6234 /// output queue nor the stream's `kawa` may be mutated or dropped until
6235 /// `io_slices` is emptied, which [`h2_transmit::confirm`] does first.
6236 pub unsafe fn gather_transmit<L>(
6237 &self,
6238 context: &Context<L>,
6239 stream_id: H2StreamId,
6240 io_slices: &mut Vec<std::io::IoSlice<'static>>,
6241 ) -> (usize, usize)
6242 where
6243 L: ListenerHandler + L7ListenerHandler,
6244 {
6245 let H2StreamId::Other { gid, .. } = stream_id else {
6246 unreachable!("no transmit names the connection's input buffer");
6247 };
6248 let stream = &context.streams[gid];
6249 // The WRITE side of `Stream::split`: a server writes the response
6250 // from `back`, a client the request from `front`.
6251 let kawa = match self.position {
6252 Position::Client(..) => &stream.front,
6253 Position::Server => &stream.back,
6254 };
6255 // SAFETY: forwarded, see this function's contract.
6256 unsafe { h2_transmit::gather_after(self.output.pending(), kawa, io_slices) }
6257 }
6258
6259 /// The buffer an [`H2WriteTarget::Transmit`] names.
6260 ///
6261 /// The write half of the same surface, and the one a driver brackets with
6262 /// [`Self::gather_transmit`] and [`h2_transmit::confirm`] around its own
6263 /// vectored write. See `write_buffer` for why it takes `context` apart
6264 /// from `self`.
6265 pub fn write_buffer<'a, L>(
6266 &'a mut self,
6267 context: &'a mut Context<L>,
6268 stream_id: H2StreamId,
6269 ) -> &'a mut GenericHttpStream
6270 where
6271 L: ListenerHandler + L7ListenerHandler,
6272 {
6273 write_buffer(
6274 &mut self.zero,
6275 &mut context.streams,
6276 &self.position,
6277 stream_id,
6278 )
6279 }
6280
6281 /// The queued output bytes an [`H2ControlFlushTarget::FlushOutput`] is
6282 /// asking the caller to move, or empty when there are none left.
6283 pub fn output_pending(&self) -> &[u8] {
6284 self.output.pending()
6285 }
6286
6287 /// Whether [`Self::output_pending`] ends with the last bytes this
6288 /// connection will ever send, so the caller may queue the TLS
6289 /// `close_notify` behind them and flush both at once.
6290 ///
6291 /// Only [`Self::goaway`] sets [`H2State::GoAway`], and only once its final
6292 /// GOAWAY is queued; the state's writable arm then closes
6293 /// (`h2_close::goaway_close_action`). Every other condition rules out a
6294 /// write the same pass would still make after this flush:
6295 ///
6296 /// - no stream left: no stream frame follows, and no stream is parked
6297 /// (`H2StreamTable::remove` nulls its park);
6298 /// - no WINDOW_UPDATE or RST_STREAM queued: the preamble already moved
6299 /// every one of them into the output this flush drains;
6300 /// - `Position::Server`: `close_notify` is a frontend TLS alert, as in
6301 /// [`H2Shell::initiate_close_notify`];
6302 /// - `close_notify_sent` unset: the alert is queued once.
6303 fn output_flush_closes_connection(&self) -> bool {
6304 self.position.is_server()
6305 && matches!(self.state, H2State::GoAway)
6306 && !self.close_notify_sent
6307 && self.stream_table.streams().is_empty()
6308 && self.flow_control.pending_window_updates_is_empty()
6309 && !self.control_tx.has_pending()
6310 }
6311
6312 /// Account for one write of [`Self::output_pending`], and answer whether
6313 /// the caller should stop.
6314 ///
6315 /// `true` means the write stalled (WouldBlock or a zero-length write) and
6316 /// the caller must wait for its next writable event; `false` means carry
6317 /// on until [`Self::output_pending`] is empty.
6318 pub fn consume_output_flush(&mut self, size: usize, status: SocketResult) -> bool {
6319 self.note_write_status(status);
6320 self.consume_output(size);
6321 update_readiness_after_write(size, status, &mut self.readiness)
6322 }
6323
6324 /// [`H2Shell::has_pending_write`] with the TLS answer supplied instead of
6325 /// queried — the byte-in / byte-out spelling of the same probe.
6326 ///
6327 /// A driver with no TLS layer passes `false` and gets the connection's own
6328 /// two conditions back.
6329 pub fn has_pending_write_with(&self, tls_wants_write: bool) -> bool {
6330 if self.peer_gone_after_final_goaway() {
6331 return false;
6332 }
6333 self.stream_table.expect_write().is_some() || !self.output.is_empty() || tls_wants_write
6334 }
6335
6336 pub fn has_pending_control_write(&self) -> bool {
6337 self.control_tx.has_pending()
6338 }
6339
6340 /// Number of streams currently tracked in the wire map. `stream_table` is
6341 /// private to this module, so this is the sole accessor for `router.rs`'s
6342 /// H2-connection-reuse heuristic (picks the non-draining H2 backend
6343 /// connection with the fewest active streams).
6344 pub fn stream_count(&self) -> usize {
6345 self.stream_table.len()
6346 }
6347
6348 pub fn create_stream<L>(
6349 &mut self,
6350 stream_id: StreamId,
6351 context: &mut Context<L>,
6352 ) -> Option<GlobalStreamId>
6353 where
6354 L: ListenerHandler + L7ListenerHandler,
6355 {
6356 // RFC 9113 §6.8: reject new streams on a draining connection
6357 if self.drain.draining() {
6358 error!(
6359 "{} Rejecting new stream {} on draining connection",
6360 log_context!(self),
6361 stream_id
6362 );
6363 return None;
6364 }
6365 let streams_before = self.stream_table.len();
6366 // Track the highest peer-initiated stream ID for GoAway frames
6367 // before any early return, so GoAway always reports the correct last
6368 // stream. `observe_peer_stream_id` asserts the monotonic-non-decreasing
6369 // property itself.
6370 self.stream_table.observe_peer_stream_id(stream_id);
6371 // `Ulid::generate()` would read the wall clock and a thread-local RNG
6372 // from inside `rusty_ulid` — two host reaches no grep over this module
6373 // can see. `Context::next_request_id` composes the same ULID from the
6374 // two sources the embedder owns instead.
6375 let request_id = context.next_request_id();
6376 let global_stream_id =
6377 context.create_stream(request_id, self.peer_settings.settings_initial_window_size)?;
6378 self.last_stream_id = (stream_id + 2) & !1;
6379 self.stream_table
6380 .register(stream_id, global_stream_id, self.now);
6381 // Post-conditions: the stream is now reachable in both indices (see
6382 // `H2StreamTable::register`'s own postconditions), the active count
6383 // grew by exactly one (the id was not already present —
6384 // `handle_header_state` rejects re-used ids), and `last_stream_id` is
6385 // the even watermark just past this id so `new_stream_id` never collides.
6386 debug_assert_eq!(
6387 self.stream_table.len(),
6388 streams_before + 1,
6389 "create_stream must add exactly one stream (id must not pre-exist)"
6390 );
6391 debug_assert!(
6392 self.last_stream_id > stream_id && self.last_stream_id & 1 == 0,
6393 "last_stream_id watermark must be the even value strictly above stream_id"
6394 );
6395 Some(global_stream_id)
6396 }
6397
6398 pub fn new_stream_id(&mut self) -> Option<StreamId> {
6399 let watermark_before = self.last_stream_id;
6400 let (issued, next) = next_stream_id(self.last_stream_id, self.position.is_client())?;
6401 self.last_stream_id = next;
6402 // Post-conditions: the locally-issued id has the parity of our role and
6403 // the watermark advanced strictly (so the next allocation cannot reuse
6404 // this id). `next_stream_id` already asserts parity vs `is_client`; here
6405 // we re-assert against `self.position` and the watermark monotonicity.
6406 debug_assert_eq!(
6407 issued & 1 == 1,
6408 self.position.is_client(),
6409 "locally-issued stream id parity must match our role"
6410 );
6411 debug_assert!(
6412 self.last_stream_id > watermark_before,
6413 "issuing a stream id must advance the watermark"
6414 );
6415 Some(issued)
6416 }
6417
6418 /// Test-only setter: jump `last_stream_id` close to `STREAM_ID_MAX` so
6419 /// that the next call to [`Self::new_stream_id`] exhausts the 31-bit
6420 /// space. FIX-22 ("Stream-ID exhaustion disconnects backend gracefully")
6421 /// exercises the `None`-return branch — reaching it through normal API
6422 /// usage would require issuing ~2³¹ requests, which is not tractable in
6423 /// an E2E harness.
6424 #[cfg(any(test, feature = "e2e-hooks"))]
6425 pub fn __test_set_last_stream_id(&mut self, id: StreamId) {
6426 self.last_stream_id = id;
6427 }
6428
6429 /// Test-only setter: map `stream_id -> gid` on the wire WITHOUT arming
6430 /// the per-stream liveness timer `stream_table.register` otherwise always
6431 /// pairs it with. `mod::tests::shutting_down_refreshes_the_snapshot_so_the_drain_budget_expires`
6432 /// needs exactly this artificial state, to force `cancel_timed_out_streams`
6433 /// down its early-return path (empty activity map) rather than race an
6434 /// RST_STREAM into that test's unrelated drain-budget assertion.
6435 #[cfg(any(test, feature = "e2e-hooks"))]
6436 pub fn __test_insert_wire_mapping_only(&mut self, stream_id: StreamId, gid: GlobalStreamId) {
6437 self.stream_table
6438 .__test_insert_wire_mapping_only(stream_id, gid);
6439 }
6440
6441 /// Cross-field invariant sweep for the H2 connection state machine,
6442 /// asserted as a run-to-completion post-condition at the end of every
6443 /// frame-handling pass (see the call in [`Self::handle_frame`]).
6444 ///
6445 /// These are relationships between *separate* fields that no single setter
6446 /// can guarantee on its own — exactly the class of bug TigerStyle's
6447 /// `check_invariants` targets. Each one is cheap (counter compares + a few
6448 /// `HashMap` membership probes); the whole function is `#[cfg(debug_assertions)]`
6449 /// and compiles out of release entirely.
6450 ///
6451 /// Encoded invariants:
6452 /// 1. **Stream-id watermark parity**: locally-issued ids never exceed
6453 /// `STREAM_ID_MAX`; `last_stream_id` stays the even watermark (it is
6454 /// rounded to `(id + 2) & !1` and initialised to 0).
6455 /// 2. **Per-stream caches are subsets of the live stream set**:
6456 /// `stream_last_activity_at` is keyed only by currently-tracked stream
6457 /// ids — a leak here would let a removed stream keep an idle timer and
6458 /// mis-fire `cancel_timed_out_streams`. (`rst_sent` is intentionally NOT
6459 /// a subset: a queued RST for an already-removed stream is legal.)
6460 /// 3. **RST queue accounting** is checked by
6461 /// [`h2_control_tx::H2ControlTx::check_invariants`] as a post-condition
6462 /// of its own mutating methods, so it is not restated here — a second
6463 /// copy of a rule drifts from the first.
6464 /// 4. **Pending WINDOW_UPDATE bound**: the coalescing map never exceeds the
6465 /// per-connection cap derived from `max_concurrent_streams`.
6466 /// 5. **Drain/state coupling**: a terminal `GoAway`/`Error` state implies the
6467 /// connection is draining (`goaway()` sets both); the converse need not
6468 /// hold (graceful drain stays in a live state).
6469 #[cfg(debug_assertions)]
6470 fn check_invariants<L>(&self, context: &Context<L>)
6471 where
6472 L: ListenerHandler + L7ListenerHandler,
6473 {
6474 // (1) Watermark parity and bound.
6475 debug_assert!(
6476 self.last_stream_id & 1 == 0,
6477 "last_stream_id must stay an even watermark, got {}",
6478 self.last_stream_id
6479 );
6480
6481 // (2) Per-stream caches are subsets of the live stream set, and every
6482 // mapping points at a valid context slot.
6483 debug_assert!(
6484 self.stream_table
6485 .stream_last_activity_at()
6486 .keys()
6487 .all(|id| self.stream_table.streams().contains_key(id)),
6488 "stream_last_activity_at must only track currently-open stream ids"
6489 );
6490 debug_assert!(
6491 self.stream_table
6492 .streams()
6493 .values()
6494 .all(|&gid| gid < context.streams.len()),
6495 "every stream mapping must point at a valid context slot"
6496 );
6497
6498 // (4) Pending WINDOW_UPDATE coalescing map bound.
6499 debug_assert!(
6500 self.flow_control.pending_window_updates_len() <= self.max_pending_window_updates,
6501 "pending WINDOW_UPDATE map must stay within its per-connection cap"
6502 );
6503
6504 // (5) Drain/state coupling: terminal states imply draining.
6505 debug_assert!(
6506 !matches!(self.state, H2State::GoAway | H2State::Error) || self.drain.draining(),
6507 "GoAway/Error state must imply the connection is draining"
6508 );
6509 }
6510
6511 fn handle_frame<E, L>(
6512 &mut self,
6513 frame: Frame,
6514 wire_payload_len: u32,
6515 context: &mut Context<L>,
6516 endpoint: E,
6517 ) -> MuxResult
6518 where
6519 E: Endpoint,
6520 L: ListenerHandler + L7ListenerHandler,
6521 {
6522 trace!("{} {:#?}", log_context!(self), frame);
6523 // Per-frame-type RX counter. Single chokepoint covers every H2 frame
6524 // type — adding a new `Frame::*` variant fails the build inside the
6525 // helper, keeping the metric breakdown in lock-step with RFC 9113 §6.
6526 self.metric_events
6527 .push(MetricEvent::FrameReceived(frame.frame_type()));
6528 let result = match frame {
6529 Frame::Data(data) => self.handle_data_frame(data, wire_payload_len, context, endpoint),
6530 Frame::Headers(headers) => self.handle_headers_frame(headers, context, endpoint),
6531 Frame::PushPromise(_) => self.handle_push_promise_frame(),
6532 Frame::Priority(priority) => self.handle_priority_frame(priority, context, endpoint),
6533 Frame::RstStream(rst_stream) => {
6534 self.handle_rst_stream_frame(rst_stream, context, endpoint)
6535 }
6536 Frame::Settings(settings) => self.handle_settings_frame(settings, context),
6537 Frame::Ping(ping) => self.handle_ping_frame(ping),
6538 Frame::GoAway(goaway) => self.handle_goaway_frame(goaway, context, endpoint),
6539 Frame::WindowUpdate(wu) => self.handle_window_update_frame(wu, context, endpoint),
6540 Frame::PriorityUpdate(pu) => self.handle_priority_update_frame(pu),
6541 Frame::Continuation(_) => {
6542 // Unreachable: standalone CONTINUATION is rejected in
6543 // `handle_header_state` (RFC 9113 §6.10) and in-block
6544 // CONTINUATION is consumed by the inline header-parsing
6545 // path. Keep a defensive fallback that returns
6546 // PROTOCOL_ERROR rather than panicking in debug builds.
6547 self.attribute_bytes_to_overhead();
6548 warn!(
6549 "{} CONTINUATION frames are handled inline during header parsing",
6550 log_context!(self)
6551 );
6552 self.goaway(H2Error::ProtocolError)
6553 }
6554 // RFC 9113 §5.5: unknown frame types MUST be ignored and discarded.
6555 // The parser already consumed the payload; attribute the bytes
6556 // to connection-level overhead and continue.
6557 Frame::Unknown(raw) => {
6558 debug!(
6559 "{} Ignoring unknown H2 frame type {}",
6560 log_context!(self),
6561 raw
6562 );
6563 self.attribute_bytes_to_overhead();
6564 MuxResult::Continue
6565 }
6566 };
6567 // Run-to-completion post-condition: the connection-level cross-field
6568 // invariants must hold after every frame is dispatched, on success and
6569 // on the protocol-error paths alike.
6570 #[cfg(debug_assertions)]
6571 self.check_invariants(context);
6572 result
6573 }
6574
6575 /// RFC 9110 §8.6: Content-Length validation must be skipped for responses
6576 /// where the body is absent by definition:
6577 /// - Responses to HEAD requests (any status)
6578 /// - 1xx informational responses
6579 /// - 204 No Content
6580 /// - 304 Not Modified
6581 fn content_length_exempt(
6582 &self,
6583 context: &crate::protocol::kawa_h1::editor::HttpContext,
6584 ) -> bool {
6585 use crate::protocol::kawa_h1::parser::Method;
6586 // HEAD method responses (only relevant when reading backend responses)
6587 if self.position.is_client() && context.method == Some(Method::Head) {
6588 return true;
6589 }
6590 // 1xx, 204, 304 status codes
6591 if let Some(status) = context.status
6592 && ((100..200).contains(&status) || status == 204 || status == 304)
6593 {
6594 return true;
6595 }
6596 false
6597 }
6598
6599 /// What `handle_data_frame` does with the DATA payload of a backend
6600 /// response that has no content by definition (RFC 9110 §6.4.1). Only
6601 /// meaningful when reading a backend response (`Position::Client`): on
6602 /// the server side `context.status` describes the response, not the
6603 /// request being read.
6604 fn backend_response_content(
6605 context: &crate::protocol::kawa_h1::editor::HttpContext,
6606 ) -> BackendResponseContent {
6607 use crate::protocol::kawa_h1::parser::Method;
6608 if matches!(context.status, Some(204 | 304)) {
6609 BackendResponseContent::Forbidden
6610 } else if context.method == Some(Method::Head) {
6611 BackendResponseContent::Discarded
6612 } else {
6613 BackendResponseContent::Forwarded
6614 }
6615 }
6616
6617 fn handle_data_frame<E, L>(
6618 &mut self,
6619 data: parser::Data,
6620 wire_payload_len: u32,
6621 context: &mut Context<L>,
6622 mut endpoint: E,
6623 ) -> MuxResult
6624 where
6625 E: Endpoint,
6626 L: ListenerHandler + L7ListenerHandler,
6627 {
6628 // CVE-2019-9518: track empty DATA frames (no payload, no END_STREAM)
6629 if data.payload.is_empty() && !data.end_stream {
6630 self.flood_detector.record_empty_data_frame();
6631 check_flood_or_return!(self);
6632 }
6633 let Some(global_stream_id) = self.stream_table.get(data.stream_id) else {
6634 // The stream was terminated while data was expected,
6635 // probably due to automatic answer for invalid/unauthorized access.
6636 // RFC 9113 §6.9: we MUST still account for the DATA payload in
6637 // connection-level flow control using the full wire length
6638 // (including pad-length byte and padding), otherwise the window
6639 // shrinks permanently and eventually stalls the connection.
6640 let conn_threshold = self.connection_config.initial_connection_window / 2;
6641 if let Some(increment) = self
6642 .flow_control
6643 .account_received_bytes(wire_payload_len, conn_threshold)
6644 {
6645 self.queue_window_update(0, increment);
6646 self.readiness.arm_writable();
6647 }
6648 self.attribute_bytes_to_overhead();
6649 return MuxResult::Continue;
6650 };
6651 let mut slice = data.payload;
6652 let stream = &mut context.streams[global_stream_id];
6653 // Unpadded application payload size — what is forwarded to the backend
6654 // and counted against Content-Length.
6655 let content_len = slice.len();
6656 // Full wire-payload size (includes pad-length byte and padding).
6657 // RFC 9113 §5.2: padding counts against flow-control windows.
6658 let wire_len = wire_payload_len as usize;
6659 let cl_exempt = self.content_length_exempt(&stream.context);
6660 let content = if self.position.is_client() {
6661 Self::backend_response_content(&stream.context)
6662 } else {
6663 BackendResponseContent::Forwarded
6664 };
6665
6666 // Extract declared content-length and update position-aware data counter
6667 let (data_received, declared_length) = {
6668 let parts = stream.split(&self.position);
6669 *parts.data_received += content_len;
6670 let total = *parts.data_received;
6671 let declared = match parts.rbuffer.body_size {
6672 kawa::BodySize::Length(n) => Some(n),
6673 _ => None,
6674 };
6675 (total, declared)
6676 };
6677
6678 // RFC 9113 §6.9 + §5.2: credit connection-level flow control BEFORE any
6679 // early-return path. Malformed DATA still consumed the peer's send
6680 // window; without crediting it back, repeated bad streams permanently
6681 // shrink the connection window and stall unrelated streams that share
6682 // the same H2 connection. Stream-level credit can stay below — once we
6683 // RST the violating stream, its per-stream window is moot per
6684 // RFC 9113 §6.9 (the receiver discards further frames for the stream).
6685 let conn_threshold = self.connection_config.initial_connection_window / 2;
6686 if let Some(increment) = self
6687 .flow_control
6688 .account_received_bytes(wire_payload_len, conn_threshold)
6689 {
6690 self.queue_window_update(0, increment);
6691 }
6692
6693 // RFC 9110 §15.3.5, §15.4.5, RFC 9113 §8.1.1: a 204 or a 304 cannot
6694 // contain content, so DATA carrying a payload makes it malformed:
6695 // reset the backend stream before the payload is queued, so it never
6696 // reaches the client (an H1 client would read it as the start of the
6697 // next response). `content_length_exempt` skips the length checks
6698 // below for these responses, so they would not catch it. A response
6699 // to HEAD only SHOULD NOT carry content (RFC 9110 §9.3.2): its payload
6700 // is discarded below instead.
6701 if content == BackendResponseContent::Forbidden && content_len > 0 {
6702 error!(
6703 "{} DATA with a {}-byte payload on a {:?} response (RFC 9110 §6.4.1)",
6704 log_context!(self),
6705 content_len,
6706 stream.context.status
6707 );
6708 incr!(names::h2::BODILESS_RESPONSE_DATA_RESET);
6709 if !self.flow_control.pending_window_updates_is_empty() {
6710 self.readiness.arm_writable();
6711 }
6712 let result = self.reset_stream(
6713 data.stream_id,
6714 global_stream_id,
6715 context,
6716 endpoint,
6717 H2Error::ProtocolError,
6718 );
6719 self.remove_dead_stream(data.stream_id, global_stream_id);
6720 return result;
6721 }
6722
6723 // RFC 9113 §8.1.1: if Content-Length is present, total DATA payload
6724 // must not exceed the declared length (check on every frame).
6725 // RFC 9110 §8.6: skip for HEAD/1xx/204/304 responses (body absent by definition).
6726 if !cl_exempt
6727 && let Some(expected) = declared_length
6728 && data_received > expected
6729 {
6730 error!(
6731 "{} Content-Length mismatch: received {} > declared {}",
6732 log_context!(self),
6733 data_received,
6734 expected
6735 );
6736 // Pair WRITABLE arming with the queued connection-level
6737 // WINDOW_UPDATE before returning; otherwise the credit sits
6738 // until the next inbound frame on this connection.
6739 if !self.flow_control.pending_window_updates_is_empty() {
6740 self.readiness.arm_writable();
6741 }
6742 let result = self.reset_stream(
6743 data.stream_id,
6744 global_stream_id,
6745 context,
6746 endpoint,
6747 H2Error::ProtocolError,
6748 );
6749 self.remove_dead_stream(data.stream_id, global_stream_id);
6750 return result;
6751 }
6752
6753 let stream = &mut context.streams[global_stream_id];
6754 self.attribute_bytes_to_stream(&mut stream.metrics);
6755 let stream_state = stream.state;
6756 let is_unlinked = matches!(stream_state, StreamState::Unlinked);
6757 let parts = stream.split(&self.position);
6758 let kawa = parts.rbuffer;
6759 self.position.count_bytes_in(parts.metrics, content_len);
6760
6761 // Stream-level flow control (only if stream is still open).
6762 // Connection-level credit was already applied above the CL check so
6763 // malformed DATA cannot starve the connection window for other streams.
6764 if !data.end_stream {
6765 self.queue_window_update(data.stream_id, wire_payload_len);
6766 }
6767
6768 // If we have pending updates, ensure we get a writable event.
6769 // Must use signal_pending_write() — not just interest.insert() — because
6770 // under edge-triggered epoll the WRITABLE event bit may have been consumed
6771 // by a previous write cycle. Without the event bit set, filter_interest()
6772 // returns 0 and the WINDOW_UPDATEs never get flushed, stalling the client.
6773 if !self.flow_control.pending_window_updates_is_empty() {
6774 self.readiness.arm_writable();
6775 }
6776
6777 // Refresh per-stream idle timer on non-empty DATA.
6778 // Empty DATA frames (CVE-2019-9518 vector) must NOT reset the timer,
6779 // otherwise an attacker can keep a stream alive indefinitely with
6780 // zero-length frames while pinning a MAX_CONCURRENT_STREAMS slot.
6781 if content_len > 0 {
6782 self.stream_table.touch_activity(data.stream_id, self.now);
6783 }
6784
6785 if is_unlinked {
6786 // Backend is gone but client is still sending DATA.
6787 // Discard the data (flow control updates were already
6788 // queued above) to prevent the buffer from filling up.
6789 kawa.storage.clear();
6790 if data.end_stream {
6791 kawa.parsing_phase = kawa::ParsingPhase::Terminated;
6792 self.mark_end_of_stream(stream);
6793 }
6794 } else {
6795 // A response to HEAD has no content (RFC 9110 §9.3.2): its
6796 // payload is never queued, as kawa's H1 parser leaves the bytes
6797 // an H1 backend sends after such a head unread, and its windows
6798 // were credited above.
6799 let discarded = content == BackendResponseContent::Discarded && content_len > 0;
6800 if discarded {
6801 // The frame's wire payload is the tail of the buffer: drop
6802 // it there. Kept behind `storage.head`, it would hold space
6803 // only the frontend consuming a block frees, and no block
6804 // refers to it, so a payload larger than the buffer would
6805 // stop the backend connection from being read before its
6806 // END_STREAM.
6807 debug_assert_eq!(
6808 kawa.storage.end,
6809 kawa.storage.head + wire_len,
6810 "a DATA payload being read is the tail of the stream buffer"
6811 );
6812 kawa.storage.end = kawa.storage.head;
6813 } else {
6814 // Advance storage.head by the full wire payload length so the
6815 // next frame doesn't read stale pad-length+padding bytes.
6816 slice.start = slice.start.saturating_add(kawa.storage.head as u32);
6817 kawa.storage.head += wire_len;
6818 }
6819 // Emit chunk framing for chunked transfer encoding (H2→H1 path).
6820 // H2 converter ignores ChunkHeader and end_chunk Flags, so this is safe for H2→H2.
6821 if kawa.body_size == kawa::BodySize::Chunked && content_len > 0 && !discarded {
6822 let hex_len = {
6823 let mut buf = Vec::with_capacity(16);
6824 let _ = write!(buf, "{content_len:x}");
6825 buf
6826 };
6827 kawa.push_block(kawa::Block::ChunkHeader(kawa::ChunkHeader {
6828 length: kawa::Store::from_vec(hex_len),
6829 }));
6830 }
6831
6832 if discarded {
6833 debug!(
6834 "{} DATA payload of {} bytes on a response to HEAD discarded",
6835 log_context!(self),
6836 content_len
6837 );
6838 } else {
6839 kawa.push_block(kawa::Block::Chunk(kawa::Chunk {
6840 data: kawa::Store::Slice(slice),
6841 }));
6842 }
6843
6844 if kawa.body_size == kawa::BodySize::Chunked && content_len > 0 && !discarded {
6845 kawa.push_block(kawa::Block::Flags(kawa::Flags {
6846 end_body: false,
6847 end_chunk: true,
6848 end_header: false,
6849 end_stream: false,
6850 }));
6851 }
6852
6853 if data.end_stream {
6854 // RFC 9113 §8.1.1: on end_stream, total DATA must equal Content-Length.
6855 // RFC 9110 §8.6: skip for HEAD/1xx/204/304 responses.
6856 if !cl_exempt
6857 && let Some(expected) = declared_length
6858 && data_received != expected
6859 {
6860 error!(
6861 "{} Content-Length mismatch: received {} != declared {}",
6862 log_context!(self),
6863 data_received,
6864 expected
6865 );
6866 let result = self.reset_stream(
6867 data.stream_id,
6868 global_stream_id,
6869 context,
6870 endpoint,
6871 H2Error::ProtocolError,
6872 );
6873 self.remove_dead_stream(data.stream_id, global_stream_id);
6874 return result;
6875 }
6876 let is_chunked = kawa.body_size == kawa::BodySize::Chunked;
6877 kawa.push_block(kawa::Block::Flags(kawa::Flags {
6878 end_body: true,
6879 end_chunk: is_chunked,
6880 end_header: false,
6881 end_stream: true,
6882 }));
6883 kawa.parsing_phase = kawa::ParsingPhase::Terminated;
6884 self.mark_end_of_stream(stream);
6885 }
6886 if let StreamState::Linked(token) = stream_state {
6887 // Mirror of `ConnectionH1::readable`'s close-delimited EOF branch
6888 // for the H2-backend → H2-frontend path: edge-triggered epoll will
6889 // NOT re-fire for bytes we just pushed into stream.back; the
6890 // synthetic event is the only wake path. LIFECYCLE invariant 15.
6891 endpoint.readiness_mut(token).arm_writable();
6892 self.metric_events
6893 .push(MetricEvent::WritableRearmedPeerData);
6894 }
6895 }
6896 MuxResult::Continue
6897 }
6898
6899 fn handle_headers_frame<E, L>(
6900 &mut self,
6901 headers: Headers,
6902 context: &mut Context<L>,
6903 mut endpoint: E,
6904 ) -> MuxResult
6905 where
6906 E: Endpoint,
6907 L: ListenerHandler + L7ListenerHandler,
6908 {
6909 // HEADERS frames represent real application activity (new request
6910 // or response). Reset the timeout since the peer is actively
6911 // communicating, unlike control frames (PING, WINDOW_UPDATE).
6912 self.arm_timeout();
6913 if !headers.end_headers {
6914 // CVE-2024-27316: only initialize tracking on the very first HEADERS
6915 // fragment, not on re-entries from ContinuationFrame (which call
6916 // handle_frame(Frame::Headers) with the accumulated header block).
6917 self.flood_detector
6918 .begin_header_block_if_new(headers.header_block_fragment.len);
6919 if !self.header_reassembly.is_in_progress() {
6920 // First HEADERS frame of this block: copy its fragment out of
6921 // `zero.storage` now, before the caller's `expect_header()`
6922 // starts overwriting it with the next frame's bytes. A
6923 // re-entry from `(H2State::ContinuationFrame(headers), _)`
6924 // already appended THIS frame's own payload to the
6925 // accumulator before calling back in here (see that match
6926 // arm in `handle_read()`), so there is nothing left to copy.
6927 //
6928 // `data_opt` (bounds-checked), not `data` (panics on OOB):
6929 // `header_block_fragment` is network-facing-derived — the
6930 // parser computes it correctly by construction, so this
6931 // should never actually be out of bounds, but "no panic on
6932 // network-facing input" (CLAUDE.md) applies regardless, and
6933 // the CVE-2024-27316 abort path below this one used to make
6934 // exactly this check before this step replaced its own
6935 // buffer access with `HeaderBlockAccumulator::finish()`
6936 // (owned bytes, no slicing left to bound-check there).
6937 let Some(fragment) = headers
6938 .header_block_fragment
6939 .data_opt(self.zero.storage.buffer())
6940 else {
6941 error!(
6942 "{} header_block_fragment out of bounds of zero.storage",
6943 log_context!(self)
6944 );
6945 return self.goaway(H2Error::InternalError);
6946 };
6947 self.header_reassembly.begin(fragment);
6948 self.zero.storage.clear();
6949 }
6950 debug!(
6951 "{} FRAGMENT: stream_id={}, accumulated_len={}",
6952 log_context!(self),
6953 headers.stream_id,
6954 self.header_reassembly.len()
6955 );
6956 self.state = H2State::ContinuationHeader(headers);
6957 return MuxResult::Continue;
6958 }
6959 // Header block is complete — reset CONTINUATION counters
6960 self.flood_detector.reset_continuation();
6961 // can this fail?
6962 let stream_id = headers.stream_id;
6963 let Some(global_stream_id) = self.stream_table.get(stream_id) else {
6964 error!(
6965 "{} Handling Headers frame with no attached stream {:#?}",
6966 log_context!(self),
6967 self
6968 );
6969 self.metric_events.push(MetricEvent::HeadersWithoutStream);
6970 self.attribute_bytes_to_overhead();
6971 return self.force_disconnect();
6972 };
6973
6974 // Refresh per-stream idle timer on HEADERS (response headers or trailers
6975 // on an existing stream). Initial HEADERS that create the stream already
6976 // set the timestamp in create_stream().
6977 self.stream_table.touch_activity(stream_id, self.now);
6978
6979 if let Some(priority) = &headers.priority
6980 && self.scheduler.push_priority(stream_id, priority.clone())
6981 {
6982 // This HEADERS frame's own block just completed (single-frame,
6983 // or the final CONTINUATION of a multi-frame sequence) — but
6984 // we're aborting before ever reaching the `finish()` call below.
6985 // If a CONTINUATION sequence fed it, `header_reassembly` is
6986 // still `is_in_progress() == true`; retire it here or it leaks
6987 // that flag into the NEXT HEADERS frame processed on this
6988 // connection, which may belong to a completely different
6989 // stream — that frame's own "read from zero.storage" fast path
6990 // would then be skipped in favour of these stale, already-
6991 // discarded bytes (sozu-proxy/sozu review of e1c3c2fb, B1).
6992 // Regression:
6993 // `a_priority_self_dependency_reset_does_not_leak_the_reassembly_accumulator`.
6994 if self.header_reassembly.is_in_progress() {
6995 self.header_reassembly.finish();
6996 }
6997 self.reset_stream(
6998 stream_id,
6999 global_stream_id,
7000 context,
7001 endpoint,
7002 H2Error::ProtocolError,
7003 );
7004 self.remove_dead_stream(stream_id, global_stream_id);
7005 return MuxResult::Continue;
7006 }
7007
7008 let stream = &mut context.streams[global_stream_id];
7009 self.attribute_bytes_to_stream(&mut stream.metrics);
7010 // The field-block bytes live in one of two places depending on how
7011 // we got here: the owned reassembly accumulator when a CONTINUATION
7012 // sequence fed this HEADERS (`header_reassembly.is_in_progress()`),
7013 // or straight out of `zero.storage` for the single-frame fast path
7014 // — the common case, which never touches the accumulator at all.
7015 // See `h2_header_reassembly.rs`.
7016 let buffer: &[u8] = if self.header_reassembly.is_in_progress() {
7017 self.header_reassembly.data()
7018 } else {
7019 // Invariant: only reachable when nothing is accumulating for
7020 // THIS stream's own block. Every early-return path above this
7021 // point that could have left a CONTINUATION-fed reassembly
7022 // `is_in_progress()` now retires it first (see the priority
7023 // self-dependency branch above — B1 in the review of e1c3c2fb
7024 // was exactly a return that skipped this and leaked the flag
7025 // into the next HEADERS frame's decode).
7026 debug_assert!(!self.header_reassembly.is_in_progress());
7027 headers
7028 .header_block_fragment
7029 .data(self.zero.storage.buffer())
7030 };
7031 let stream = &mut context.streams[global_stream_id];
7032 let parts = &mut stream.split(&self.position);
7033 let was_initial = parts.rbuffer.is_initial();
7034 let elide_x_real_ip = parts.context.elide_x_real_ip;
7035 let (header_events, status) = pkawa::handle_header(
7036 self.hpack.decoder_mut(),
7037 self.scheduler.prioriser_mut(),
7038 stream_id,
7039 parts.rbuffer,
7040 buffer,
7041 headers.end_stream,
7042 parts.context,
7043 self.flood_detector.config().max_header_list_size(),
7044 self.flood_detector.config().max_header_fields(),
7045 elide_x_real_ip,
7046 );
7047 self.metric_events.extend(header_events);
7048 if self.header_reassembly.is_in_progress() {
7049 self.header_reassembly.finish();
7050 }
7051 self.zero.storage.clear();
7052 if let Err((error, global)) = status {
7053 match self.position {
7054 Position::Client(..) => self
7055 .metric_events
7056 .push(MetricEvent::BackendHeaderParseError),
7057 Position::Server => self
7058 .metric_events
7059 .push(MetricEvent::FrontendHeaderParseError),
7060 }
7061 if global {
7062 error!(
7063 "{} GOT GLOBAL ERROR WHILE PROCESSING HEADERS",
7064 log_context!(self)
7065 );
7066 return self.goaway(error);
7067 } else {
7068 let result =
7069 self.reset_stream(stream_id, global_stream_id, context, endpoint, error);
7070 self.remove_dead_stream(stream_id, global_stream_id);
7071 return result;
7072 }
7073 }
7074 if headers.end_stream {
7075 // RFC 9113 §8.1.1: when END_STREAM arrives via trailers,
7076 // validate that total DATA received matches Content-Length.
7077 // RFC 9110 §8.6: skip for HEAD/1xx/204/304 responses.
7078 if !was_initial && !self.content_length_exempt(&stream.context) {
7079 let parts = stream.split(&self.position);
7080 if let kawa::BodySize::Length(expected) = parts.rbuffer.body_size
7081 && *parts.data_received != expected
7082 {
7083 error!(
7084 "{} Content-Length mismatch on trailers: received {} != declared {}",
7085 log_context!(self),
7086 *parts.data_received,
7087 expected
7088 );
7089 let result = self.reset_stream(
7090 stream_id,
7091 global_stream_id,
7092 context,
7093 endpoint,
7094 H2Error::ProtocolError,
7095 );
7096 self.remove_dead_stream(stream_id, global_stream_id);
7097 return result;
7098 }
7099 }
7100 self.mark_end_of_stream(stream);
7101 }
7102 if let StreamState::Linked(token) = stream.state {
7103 // Mirror of handle_data_frame's rearm. LIFECYCLE invariant 15.
7104 endpoint.readiness_mut(token).arm_writable();
7105 self.metric_events
7106 .push(MetricEvent::WritableRearmedPeerHeaders);
7107 }
7108 if was_initial && self.position.is_client() {
7109 // The backend's response field block has just been decoded and
7110 // parsed: the client twin of the `is_server()` branch below, on
7111 // the same first-HEADERS edge (`was_initial` is what keeps a
7112 // trailer block out of it). This is the whole of
7113 // `backend_header_time`'s H2 arming — nginx's
7114 // `$upstream_header_time` (sozu-proxy/sozu#426).
7115 //
7116 // Unconditional, including on a 1xx: `ConnectionH2::handle_1xx_reset`
7117 // clears the back buffer once the informational response has been
7118 // forwarded, so the FINAL response returns to the initial parsing
7119 // phase and re-enters this edge, overwriting. That is the response
7120 // `SessionMetrics::backend_stop` also anchors on.
7121 stream.metrics.backend_headers_received();
7122 }
7123 // was_initial prevents trailers from triggering connection
7124 if was_initial && self.position.is_server() {
7125 self.metric_events.push(MetricEvent::RequestStarted);
7126 stream.metrics.service_start();
7127 stream.request_counted = true;
7128 stream.state = StreamState::Link;
7129 context.pending_links.push_back(global_stream_id);
7130 }
7131 MuxResult::Continue
7132 }
7133
7134 fn handle_push_promise_frame(&mut self) -> MuxResult {
7135 self.attribute_bytes_to_overhead();
7136 match self.position {
7137 Position::Client(..) => {
7138 // RFC 9113 §8.4: Server push is deprecated. Sozu never sends
7139 // SETTINGS_ENABLE_PUSH=1, so receiving PUSH_PROMISE is a protocol error.
7140 error!(
7141 "{} Received PUSH_PROMISE but server push is not supported",
7142 log_context!(self)
7143 );
7144 self.goaway(H2Error::ProtocolError)
7145 }
7146 Position::Server => {
7147 // Clients must never send PUSH_PROMISE (RFC 9113 §8.4)
7148 error!("{} Received PUSH_PROMISE from client", log_context!(self));
7149 self.goaway(H2Error::ProtocolError)
7150 }
7151 }
7152 }
7153
7154 fn handle_priority_frame<E, L>(
7155 &mut self,
7156 priority: parser::Priority,
7157 context: &mut Context<L>,
7158 endpoint: E,
7159 ) -> MuxResult
7160 where
7161 E: Endpoint,
7162 L: ListenerHandler + L7ListenerHandler,
7163 {
7164 if let Some(global_stream_id) = self
7165 .stream_table
7166 .streams()
7167 .get(&priority.stream_id)
7168 .copied()
7169 {
7170 let stream = &mut context.streams[global_stream_id];
7171 self.attribute_bytes_to_stream(&mut stream.metrics);
7172 } else {
7173 self.attribute_bytes_to_overhead();
7174 }
7175 // Pass 3 Medium #4: standalone PRIORITY frames can arrive for any
7176 // peer-chosen stream ID. Accept only currently-open streams and a
7177 // small idle look-ahead window; everything else is dropped before
7178 // it can feed memory into the priority map.
7179 if self.scheduler.push_priority_guarded(
7180 priority.stream_id,
7181 priority.inner,
7182 self.last_stream_id,
7183 self.stream_table.streams(),
7184 ) {
7185 if let Some(global_stream_id) = self
7186 .stream_table
7187 .streams()
7188 .get(&priority.stream_id)
7189 .copied()
7190 {
7191 let result = self.reset_stream(
7192 priority.stream_id,
7193 global_stream_id,
7194 context,
7195 endpoint,
7196 H2Error::ProtocolError,
7197 );
7198 self.remove_dead_stream(priority.stream_id, global_stream_id);
7199 return result;
7200 } else {
7201 // RFC 7540 §5.3.1 makes a self-dependency a stream error, but
7202 // this stream is idle and RFC 9113 §6.4 forbids RST_STREAM on
7203 // an idle stream: the connection error is the only reply.
7204 error!(
7205 "{} INVALID PRIORITY RECEIVED ON INVALID STREAM",
7206 log_context!(self)
7207 );
7208 return self.goaway(H2Error::ProtocolError);
7209 }
7210 }
7211 MuxResult::Continue
7212 }
7213
7214 /// RFC 9113 §6.3: a PRIORITY frame with a length other than 5 octets is a
7215 /// stream error of type FRAME_SIZE_ERROR. An open stream is reset; on an
7216 /// idle, closed or already reset stream the frame is dropped, since
7217 /// RST_STREAM must not name an idle stream (§6.4) and PRIORITY changes no
7218 /// stream state. The frame's payload was already consumed.
7219 fn handle_priority_frame_size_error<E, L>(
7220 &mut self,
7221 stream_id: StreamId,
7222 context: &mut Context<L>,
7223 endpoint: E,
7224 ) -> MuxResult
7225 where
7226 E: Endpoint,
7227 L: ListenerHandler + L7ListenerHandler,
7228 {
7229 self.metric_events
7230 .push(MetricEvent::FrameReceived(FrameType::Priority));
7231 self.attribute_bytes_to_overhead();
7232 if let Some(global_stream_id) = self.stream_table.get(stream_id)
7233 && context.streams[global_stream_id].state.is_open()
7234 && !self.stream_table.rst_sent_contains(stream_id)
7235 {
7236 debug!(
7237 "{} PRIORITY frame of invalid length on stream {}, sending RST_STREAM(FRAME_SIZE_ERROR)",
7238 log_context!(self),
7239 stream_id
7240 );
7241 let result = self.reset_stream(
7242 stream_id,
7243 global_stream_id,
7244 context,
7245 endpoint,
7246 H2Error::FrameSizeError,
7247 );
7248 self.remove_dead_stream(stream_id, global_stream_id);
7249 return result;
7250 }
7251 debug!(
7252 "{} Ignoring PRIORITY frame of invalid length on stream {}",
7253 log_context!(self),
7254 stream_id
7255 );
7256 self.flood_detector.record_glitch();
7257 check_flood_or_return!(self);
7258 MuxResult::Continue
7259 }
7260
7261 /// RFC 9218 §7.1: PRIORITY_UPDATE reprioritizes an open or idle-soon
7262 /// stream at the connection level. Decodes the priority field value
7263 /// (same grammar as the `priority` request header, `parse_rfc9218_priority`)
7264 /// and pushes it into the `Prioriser` through the same guarded path used
7265 /// for standalone PRIORITY frames — the guard bounds memory against a
7266 /// client spamming PRIORITY_UPDATE for far-future stream IDs.
7267 ///
7268 /// Prioritized stream ID `0` is a connection-level `PROTOCOL_ERROR`
7269 /// (RFC 9218 §7.1). For any other ID that is not currently open or
7270 /// within the idle look-ahead budget, the update is silently dropped
7271 /// (matches the PRIORITY-frame guard semantics — no state change).
7272 fn handle_priority_update_frame(&mut self, pu: parser::PriorityUpdate) -> MuxResult {
7273 self.attribute_bytes_to_overhead();
7274 if pu.prioritized_stream_id == 0 {
7275 error!(
7276 "{} PRIORITY_UPDATE with prioritized_stream_id=0 (RFC 9218 §7.1)",
7277 log_context!(self)
7278 );
7279 return self.goaway(H2Error::ProtocolError);
7280 }
7281 let (urgency, incremental) = pkawa::parse_rfc9218_priority(&pu.priority_field_value);
7282 let (prev_urgency, _) = self.scheduler.priority(&pu.prioritized_stream_id);
7283 trace!(
7284 "{} PRIORITY_UPDATE stream={} urgency={}->{} incremental={} rearmed_writable=true",
7285 log_context!(self),
7286 pu.prioritized_stream_id,
7287 prev_urgency,
7288 urgency,
7289 incremental
7290 );
7291 let _ = self.scheduler.push_priority_guarded(
7292 pu.prioritized_stream_id,
7293 parser::PriorityPart::Rfc9218 {
7294 urgency,
7295 incremental,
7296 },
7297 self.last_stream_id,
7298 self.stream_table.streams(),
7299 );
7300 // LIFECYCLE invariant 15: reprioritisation only changes ordering for
7301 // the NEXT write pass. Under ET epoll, if finalize_write already
7302 // stripped WRITABLE, the scheduler won't re-run without a synthetic
7303 // wake — pair the interest insert with signal_pending_write.
7304 self.readiness.arm_writable();
7305 self.metric_events
7306 .push(MetricEvent::WritableRearmedPriorityUpdate);
7307 MuxResult::Continue
7308 }
7309
7310 fn handle_rst_stream_frame<E, L>(
7311 &mut self,
7312 rst_stream: parser::RstStream,
7313 context: &mut Context<L>,
7314 mut endpoint: E,
7315 ) -> MuxResult
7316 where
7317 E: Endpoint,
7318 L: ListenerHandler + L7ListenerHandler,
7319 {
7320 // Per-error-code counter for the inbound RST. Emitted before the
7321 // flood-detector trip check so even a connection that gets terminated
7322 // by `handle_flood_violation` shows up in the per-code breakdown
7323 // (the dedicated `h2.flood.violation.rst_stream_*` series tracks the
7324 // mitigation event itself).
7325 self.metric_events
7326 .push(MetricEvent::RstStreamReceived(rst_stream.error_code));
7327 // CVE-2023-44487 Rapid Reset + CVE-2019-9514: track RST_STREAM rate.
7328 self.flood_detector.record_rst_stream_window();
7329 check_flood_or_return!(self);
7330 // Additional CVE-2023-44487 mitigation: connection-lifetime caps on
7331 // RST_STREAM frames received. The per-window counter above
7332 // half-decays, so a patient client could keep resetting forever; the
7333 // lifetime counters cap that, relative to the streams a backend
7334 // answered so benign cancels never accumulate toward a fixed ceiling.
7335 // Streams whose backend response has not yet started count toward a
7336 // much lower "abusive" cap (more than half of the streams, past a
7337 // floor) — the Rapid Reset signature, where the attacker pays one RST
7338 // frame and we pay a backend round-trip for each.
7339 //
7340 // "Response started" here means the Server has begun producing
7341 // response bytes (backend kawa buffer past its initial phase). On a
7342 // backend connection (`Position::Client`) the Rapid Reset signature
7343 // does not exist: Sōzu opened the stream, and a backend resetting it
7344 // before answering — `REFUSED_STREAM` under load, a `CANCEL` from its
7345 // own timeout — costs the backend, not Sōzu. Those resets never count
7346 // as pre-response.
7347 //
7348 // On a frontend connection, a stream reset after its response started
7349 // is an answered stream: it counts toward the caps' denominator
7350 // (`routed_to_a_backend`) before the caps are checked, so a client
7351 // that cancels every stream after its response never has one more
7352 // reset than answered streams.
7353 // A stream refused under flood pressure never got a response:
7354 // resetting it is a pre-response reset, whatever the refusal did to
7355 // the stream table. It also shows the peer does not honour
7356 // REFUSED_STREAM, so the soft refusal ends for this connection: more
7357 // refusals would only multiply the RST_STREAM frames Sōzu sends ahead
7358 // of the cap's GOAWAY.
7359 let refused_stream_reset = self.position.is_server()
7360 && self.stream_table.get(rst_stream.stream_id).is_none()
7361 && self
7362 .flood_refused_streams
7363 .is_some_and(|(first, last)| (first..=last).contains(&rst_stream.stream_id));
7364 if refused_stream_reset {
7365 self.flood_refusal_ignored = true;
7366 }
7367 let (response_started, answered) =
7368 match self.stream_table.streams().get(&rst_stream.stream_id) {
7369 Some(global_stream_id) => {
7370 let stream = &context.streams[*global_stream_id];
7371 let started = !stream.back.is_initial();
7372 (
7373 self.position.is_client() || started,
7374 self.position.is_server() && started && routed_to_a_backend(stream),
7375 )
7376 }
7377 None if refused_stream_reset => (false, false),
7378 // Stream already gone (e.g. closed, not yet registered) —
7379 // treat as response-started to avoid over-counting benign
7380 // races as abusive.
7381 None => (true, false),
7382 };
7383 if let Some(violation) =
7384 self.flood_detector
7385 .record_rst_received(response_started, answered, self.now)
7386 {
7387 return self.handle_flood_violation(violation);
7388 }
7389 // Rapid Reset signature (CVE-2023-44487): a RST that arrives before the
7390 // backend has begun answering. Emitted alongside the per-code counter
7391 // so the SOC can alert on the rate of pre-response RSTs without
7392 // having to differentiate by error code.
7393 if !response_started {
7394 self.metric_events
7395 .push(MetricEvent::RstStreamReceivedBeforeResponse);
7396 }
7397 debug!(
7398 "{} RstStream({} -> {})",
7399 log_context!(self),
7400 rst_stream.error_code,
7401 H2Error::try_from(rst_stream.error_code).map_or("UNKNOWN_ERROR", |e| e.as_str())
7402 );
7403 // Compute totals before removing the stream from the map,
7404 // so the removed stream's bytes are included in the total.
7405 let rst_byte_totals = self.compute_stream_byte_totals(context);
7406 if let Some(global_stream_id) = self
7407 .stream_table
7408 .streams()
7409 .get(&rst_stream.stream_id)
7410 .copied()
7411 {
7412 let stream = &mut context.streams[global_stream_id];
7413 self.attribute_bytes_to_stream(&mut stream.metrics);
7414 let linked_token = stream.linked_token();
7415 let (client_rtt, server_rtt) = self.snapshot_rtts(&endpoint, linked_token);
7416 if let Some(token) = linked_token {
7417 endpoint.end_stream(token, global_stream_id, context);
7418 }
7419 let stream = &mut context.streams[global_stream_id];
7420 match &self.position {
7421 // Inbound RST_STREAM on the backend side terminates the in-flight
7422 // request without going through Connection::end_stream (the normal
7423 // place where Backend.active_requests is decremented), so do the
7424 // bookkeeping explicitly here to avoid leaking load counters.
7425 Position::Client(_, backend, BackendStatus::Connected) => {
7426 context.record_backend_delta(backend, BackendChange::StreamsEnded(1));
7427 }
7428 Position::Client(..) => {}
7429 Position::Server => {
7430 self.distribute_overhead(&mut stream.metrics, rst_byte_totals);
7431 // This is a special case, normally, all stream are terminated by the server
7432 // when the last byte of the response is written. Here, the reset is requested
7433 // on the server endpoint and immediately terminates, shortcutting the other path
7434 stream.metrics.backend_stop();
7435 let events = stream.generate_access_log(
7436 true,
7437 Some("H2::ResetFrame"),
7438 context.listener.clone(),
7439 client_rtt,
7440 server_rtt,
7441 );
7442 self.metric_events.extend(events.into_iter().flatten());
7443 stream.state = StreamState::Recycle;
7444 }
7445 }
7446 // Retire from streams/prioriser/stream_last_activity_at and
7447 // invalidate expect_write/expect_read if they reference this gid.
7448 self.remove_dead_stream(rst_stream.stream_id, global_stream_id);
7449 } else {
7450 self.attribute_bytes_to_overhead();
7451 }
7452 MuxResult::Continue
7453 }
7454
7455 fn handle_settings_frame<L>(
7456 &mut self,
7457 settings: parser::Settings,
7458 context: &mut Context<L>,
7459 ) -> MuxResult
7460 where
7461 L: ListenerHandler + L7ListenerHandler,
7462 {
7463 if settings.ack {
7464 // RFC 9113 §6.5: SETTINGS ACK must have empty payload
7465 if !settings.settings.is_empty() {
7466 error!("{} SETTINGS ACK with non-empty payload", log_context!(self));
7467 return self.goaway(H2Error::FrameSizeError);
7468 }
7469 // RFC 9113 §6.5: peer acknowledged our SETTINGS — clear timeout
7470 self.settings_sent_at = None;
7471 // RFC 7541 §4.2: sync the decoder's max allowed table size with
7472 // what we advertised. Currently a no-op (settings don't change at
7473 // runtime), but guards against future runtime SETTINGS updates.
7474 self.hpack.set_decoder_max_allowed_table_size(
7475 self.local_settings.settings_header_table_size as usize,
7476 );
7477 self.attribute_bytes_to_overhead();
7478 return MuxResult::Continue;
7479 }
7480 // CVE-2019-9515: track SETTINGS frame rate
7481 self.flood_detector.record_settings_frame();
7482 check_flood_or_return!(self);
7483 for setting in settings.settings {
7484 let v = setting.value;
7485 let mut is_error = false;
7486 #[rustfmt::skip]
7487 match setting.identifier {
7488 parser::SETTINGS_HEADER_TABLE_SIZE => {
7489 // Cap to the configured maximum — a malicious peer can
7490 // advertise up to 4 GB to inflate HPACK encoder memory.
7491 let cap = self.flood_detector.config().max_header_table_size();
7492 let capped = v.min(cap);
7493 self.peer_settings.settings_header_table_size = capped;
7494 self.hpack.set_encoder_max_table_size(capped as usize);
7495 // RFC 7541 §4.2 / §6.3: queue a dynamic-table-size-update
7496 // HPACK directive for the next header block we emit.
7497 // Without it, the peer's decoder keeps its previous (possibly
7498 // larger) table cap and our encoder-side change is silent
7499 // — conformance suites (h2spec `hpack/4.2`) will flag it.
7500 self.pending_table_size_update = Some(capped);
7501 },
7502 parser::SETTINGS_ENABLE_PUSH => { self.peer_settings.settings_enable_push = v == 1; is_error |= v > 1 },
7503 parser::SETTINGS_MAX_CONCURRENT_STREAMS => { self.peer_settings.settings_max_concurrent_streams = v },
7504 parser::SETTINGS_INITIAL_WINDOW_SIZE => {
7505 // RFC 9113 §6.5.2 / §6.9.2: a value above 2^31-1, or one
7506 // that pushes a stream window past it, is a connection
7507 // error of type FLOW_CONTROL_ERROR, not PROTOCOL_ERROR.
7508 if self.update_initial_window_size(v, context) {
7509 error!("{} INVALID SETTINGS_INITIAL_WINDOW_SIZE {}", log_context!(self), v);
7510 return self.goaway(H2Error::FlowControlError);
7511 }
7512 },
7513 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) },
7514 parser::SETTINGS_MAX_HEADER_LIST_SIZE => { self.peer_settings.settings_max_header_list_size = v },
7515 parser::SETTINGS_ENABLE_CONNECT_PROTOCOL => { self.peer_settings.settings_enable_connect_protocol = v == 1; is_error |= v > 1 },
7516 parser::SETTINGS_NO_RFC7540_PRIORITIES => { self.peer_settings.settings_no_rfc7540_priorities = v == 1; is_error |= v > 1 },
7517 other => { warn!("Unknown setting_id: {}, we MUST ignore this", other); self.flood_detector.record_glitch() },
7518 };
7519 if is_error {
7520 error!("{} INVALID SETTING", log_context!(self));
7521 return self.goaway(H2Error::ProtocolError);
7522 }
7523 }
7524
7525 self.attribute_bytes_to_overhead();
7526
7527 // Enlarge the connection-level receive window for backend H2
7528 // connections (Position::Client). The server side serialises it
7529 // beside its own SETTINGS in the `(ClientSettings, Server)` readable
7530 // arm, but the client needs to do it here after receiving the
7531 // server's initial SETTINGS. Once only: a later SETTINGS frame must
7532 // not grant the surplus again (RFC 9113 §6.9.1).
7533 if self.position.is_client() && !self.connection_window_enlarged {
7534 self.connection_window_enlarged = true;
7535 let increment = self
7536 .connection_config
7537 .initial_connection_window
7538 .saturating_sub(DEFAULT_INITIAL_WINDOW_SIZE);
7539 if increment > 0 {
7540 self.queue_window_update(0, increment);
7541 }
7542 // Do NOT increment flow_control.window here: sending our own
7543 // WINDOW_UPDATE enlarges the peer's send allowance, not ours.
7544 // Our send window is only updated by WINDOW_UPDATEs we receive
7545 // from the peer (RFC 9113 §6.9).
7546 }
7547
7548 self.output.push(&serializer::SETTINGS_ACKNOWLEDGEMENT);
7549 self.queue_output();
7550 MuxResult::Continue
7551 }
7552
7553 /// Hand the frame just queued in [`Self::output`] to the write side.
7554 ///
7555 /// Reading goes on: `zero` is input only, so the next frame header no
7556 /// longer lands on queued output. The frame goes out after everything
7557 /// queued before it, including the rest of a stream frame a partial
7558 /// write cut (#1604), as HAProxy appends it to its single output ring.
7559 fn queue_output(&mut self) {
7560 self.readiness.interest.insert(Ready::WRITABLE);
7561 self.readiness.signal_pending_write();
7562 }
7563
7564 /// How much queued output stops reading, as HAProxy's demux stops on a
7565 /// full `mbuf` (`H2_CF_DEM_MROOM`): one buffer's worth. Every queued
7566 /// control frame answers a frame the peer sent, so a peer that sends
7567 /// while never reading cannot grow [`Self::output`] past this.
7568 fn output_read_cap(&self) -> usize {
7569 self.zero.storage.capacity()
7570 }
7571
7572 /// Drop everything queued in [`Self::output`], and restore reading if the
7573 /// read cap had withdrawn it: the only thing reads waited for is gone.
7574 /// Every site that empties the queue goes through here or through
7575 /// [`Self::consume_output`], so `reads_wait_for_output` never outlives
7576 /// the output it waited on.
7577 fn clear_output(&mut self) {
7578 self.output.clear();
7579 if self.reads_wait_for_output {
7580 self.reads_wait_for_output = false;
7581 self.readiness.interest.insert(Ready::READABLE);
7582 }
7583 }
7584
7585 /// Account for `size` bytes the socket took from [`Self::output`], and
7586 /// restore reading once it drains below [`Self::output_read_cap`].
7587 fn consume_output(&mut self, size: usize) {
7588 let overhead = self.output.consume(size);
7589 self.metric_events
7590 .push(self.position.bytes_out_event(overhead));
7591 self.bytes.overhead_bout += overhead;
7592 if self.reads_wait_for_output && self.output.len() < self.output_read_cap() {
7593 self.reads_wait_for_output = false;
7594 self.readiness.interest.insert(Ready::READABLE);
7595 }
7596 }
7597
7598 fn handle_ping_frame(&mut self, ping: parser::Ping) -> MuxResult {
7599 if ping.ack {
7600 self.attribute_bytes_to_overhead();
7601 return MuxResult::Continue;
7602 }
7603 // CVE-2019-9512: track non-ACK PING frame rate
7604 self.flood_detector.record_ping_frame();
7605 check_flood_or_return!(self);
7606 self.attribute_bytes_to_overhead();
7607 let ping_response_size = serializer::PING_ACKNOWLEDGEMENT_HEADER.len() + 8;
7608 match self.output.push_frames(ping_response_size, |buf| {
7609 serializer::gen_ping_acknowledgement(buf, &ping.payload).map(|(_, size)| size)
7610 }) {
7611 Ok(_) => {
7612 self.metric_events.push(MetricEvent::PingAckFrameSent);
7613 }
7614 Err(error) => {
7615 error!(
7616 "{} Could not serialize PingFrame: {:?}",
7617 log_context!(self),
7618 error
7619 );
7620 return self.force_disconnect();
7621 }
7622 };
7623 self.queue_output();
7624 MuxResult::Continue
7625 }
7626
7627 fn handle_goaway_frame<E, L>(
7628 &mut self,
7629 goaway: parser::GoAway,
7630 context: &mut Context<L>,
7631 mut endpoint: E,
7632 ) -> MuxResult
7633 where
7634 E: Endpoint,
7635 L: ListenerHandler + L7ListenerHandler,
7636 {
7637 self.attribute_bytes_to_overhead();
7638 // A peer sends at most two GOAWAY frames in a graceful close (RFC
7639 // 9113 §6.8). Each one counts toward the glitch budget, so a peer
7640 // repeating GOAWAY on a connection kept open by its in-flight
7641 // streams is bounded, as is the log line below.
7642 self.flood_detector.record_glitch();
7643 check_flood_or_return!(self);
7644 let error_name =
7645 H2Error::try_from(goaway.error_code).map_or("UNKNOWN_ERROR", |e| e.as_str());
7646 if goaway.error_code == H2Error::NoError as u32 {
7647 debug!(
7648 "{} Received GOAWAY: last_stream_id={}, error={}, debug_data={:?}",
7649 log_context!(self),
7650 goaway.last_stream_id,
7651 error_name,
7652 goaway.additional_debug_data
7653 );
7654 } else {
7655 // Peer-originated failure: no variant of H2Error from a peer
7656 // implies a sozu bug. On a backend connection, impact handling
7657 // is separate (retry above `last_stream_id`, RST_STREAM for
7658 // consumed streams) and logs its own details below; a frontend
7659 // connection retires nothing. Either way the summary drops to
7660 // `warn!`.
7661 warn!(
7662 "{} Received GOAWAY: last_stream_id={}, error={}, debug_data={:?}",
7663 log_context!(self),
7664 goaway.last_stream_id,
7665 error_name,
7666 goaway.additional_debug_data
7667 );
7668 }
7669 self.metric_events
7670 .push(MetricEvent::GoAwayReceived(goaway.error_code));
7671 // RFC 9113 §6.8: begin graceful drain.
7672 self.drain.observe_peer_goaway(goaway.last_stream_id);
7673 let peer_last_stream_id = self
7674 .drain
7675 .peer_last_stream_id()
7676 .expect("observe_peer_goaway just recorded this");
7677
7678 // RFC 9113 §6.8: `last_stream_id` bounds the streams the GOAWAY
7679 // RECEIVER initiated. On a frontend connection (Position::Server)
7680 // that is server-initiated streams, which sozu never opens (no
7681 // push), so every stream here was opened by the GOAWAY sender and
7682 // is unaffected: each runs to completion, `draining` refuses any new
7683 // one (`create_stream`), and the connection closes once the last
7684 // completes (the `draining && streams().is_empty()` finalize step).
7685 //
7686 // On a backend connection (Position::Client) the streams are
7687 // sozu-initiated: those with ID > last_stream_id were NOT processed
7688 // by the peer and are retried (StreamState::Link) on a new
7689 // connection; those at or below it run to completion.
7690 // IMPORTANT: do NOT call endpoint.end_stream() here — that would
7691 // remove the stream from the frontend's H2 stream map and send
7692 // RST_STREAM to the client, killing the request instead of retrying it.
7693 let mut retry_streams = Vec::new();
7694 if matches!(self.position, Position::Client(..)) {
7695 for (&stream_id, &global_stream_id) in self.stream_table.streams() {
7696 if stream_id > peer_last_stream_id {
7697 retry_streams.push((stream_id, global_stream_id));
7698 }
7699 }
7700 }
7701 for (stream_id, global_stream_id) in &retry_streams {
7702 // `endpoint` is the frontend side (`EndpointServer`) only on a
7703 // backend connection: its `readiness_mut` is what the answers
7704 // below must arm so they reach the client.
7705 debug_assert!(
7706 matches!(self.position, Position::Client(..)),
7707 "only a backend connection retires streams on a received GOAWAY"
7708 );
7709 // Remove from reverse index before transitioning away from Linked.
7710 if let StreamState::Linked(token) = context.streams[*global_stream_id].state {
7711 remove_backend_stream(&mut context.backend_streams, token, *global_stream_id);
7712 }
7713 let stream = &mut context.streams[*global_stream_id];
7714 // A stream above `last_stream_id` was not processed (RFC 9113
7715 // §6.8), but it is only replayable if its request can be sent
7716 // again. Once this connection encoded it, `front.out` holds
7717 // frames bound to this connection's stream id and HPACK encoder
7718 // and its header blocks are gone: re-linked, those frames would
7719 // reach another backend connection of this session, possibly one
7720 // this frontend connection's other streams share (sozu#1632).
7721 if stream.front.consumed {
7722 // Request was already sent to this backend — we can't
7723 // replay it. Use the linked token's readiness (via
7724 // endpoint) so the RST_STREAM reaches the client.
7725 debug!(
7726 "{} GOAWAY: stream {} already consumed, cannot retry",
7727 log_context!(self),
7728 stream_id
7729 );
7730 if let StreamState::Linked(token) = stream.state {
7731 let front_readiness = endpoint.readiness_mut(token);
7732 forcefully_terminate_answer(stream, front_readiness, H2Error::RefusedStream);
7733 } else {
7734 warn!(
7735 "{} GOAWAY: stream {} consumed but not Linked, cannot notify frontend",
7736 log_context!(self),
7737 stream_id
7738 );
7739 }
7740 } else if stream.front_bound_to_backend {
7741 // Encoded for this backend, not sent, not processed: answered
7742 // with a whole 503 response rather than an error-terminated
7743 // one. `forcefully_terminate_answer` leaves an empty response
7744 // kawa in `Error`, which an H2 frontend turns into a
7745 // RST_STREAM but an H1 frontend cannot write at all, so its
7746 // client would wait for its timeout; this core cannot tell
7747 // which frontend it serves. 503, not 502: the backend refused
7748 // the request explicitly and did not process it (RFC 9110
7749 // §15.6.4), which is what a draining GOAWAY means.
7750 debug!(
7751 "{} GOAWAY: stream {} encoded for this backend and unsent, answering 503",
7752 log_context!(self),
7753 stream_id
7754 );
7755 if let StreamState::Linked(token) = stream.state {
7756 let answers_rc = stream.answers.clone();
7757 let answers = answers_rc.borrow();
7758 let front_readiness = endpoint.readiness_mut(token);
7759 set_default_answer(stream, front_readiness, 503, &answers);
7760 debug_assert!(
7761 stream.state == StreamState::Unlinked && !stream.back.is_error(),
7762 "a refused, unsent request gets a whole answer, never a silent error"
7763 );
7764 } else {
7765 warn!(
7766 "{} GOAWAY: stream {} encoded but not Linked, cannot notify frontend",
7767 log_context!(self),
7768 stream_id
7769 );
7770 }
7771 } else {
7772 // Pre: a stream re-linked here carries no frame of this
7773 // connection (sozu-proxy/sozu#1632).
7774 debug_assert!(
7775 stream.front.out.is_empty(),
7776 "a stream re-linked after GOAWAY holds no frame encoded for this connection"
7777 );
7778 stream.state = StreamState::Link;
7779 context.pending_links.push_back(*global_stream_id);
7780 }
7781 // Both retry (!consumed) and terminated (consumed) paths remove the
7782 // stream from self.streams without going through Connection::end_stream,
7783 // so decrement Backend.active_requests here to keep load metrics honest.
7784 if let Position::Client(_, backend, BackendStatus::Connected) = &self.position {
7785 context.record_backend_delta(backend, BackendChange::StreamsEnded(1));
7786 }
7787 // Retire from streams/prioriser/stream_last_activity_at and
7788 // invalidate expect_write/expect_read if they reference this gid.
7789 self.remove_dead_stream(*stream_id, *global_stream_id);
7790 }
7791
7792 // If no active streams remain, close immediately
7793 if self.stream_table.streams().is_empty() {
7794 return self.goaway(H2Error::NoError);
7795 }
7796
7797 // Otherwise, let the remaining streams complete: every stream on a
7798 // frontend connection, those at or below `last_stream_id` on a
7799 // backend one. The connection is closed when all streams finish.
7800 MuxResult::Continue
7801 }
7802
7803 fn handle_window_update_frame<E, L>(
7804 &mut self,
7805 wu: WindowUpdate,
7806 context: &mut Context<L>,
7807 endpoint: E,
7808 ) -> MuxResult
7809 where
7810 E: Endpoint,
7811 L: ListenerHandler + L7ListenerHandler,
7812 {
7813 let stream_id = wu.stream_id;
7814 let increment = wu.increment;
7815
7816 // RFC 9113 §6.9: increment of 0 MUST be treated as an error.
7817 // Connection-level (stream 0) -> connection error (GOAWAY).
7818 // Stream-level -> stream error (RST_STREAM).
7819 if increment == 0 {
7820 if stream_id == 0 {
7821 error!(
7822 "{} WINDOW_UPDATE with zero increment on connection (stream 0)",
7823 log_context!(self)
7824 );
7825 return self.goaway(H2Error::ProtocolError);
7826 } else {
7827 error!(
7828 "{} WINDOW_UPDATE with zero increment on stream {}",
7829 log_context!(self),
7830 stream_id
7831 );
7832 if let Some(global_stream_id) = self.stream_table.get(stream_id) {
7833 let result = self.reset_stream(
7834 stream_id,
7835 global_stream_id,
7836 context,
7837 endpoint,
7838 H2Error::ProtocolError,
7839 );
7840 self.remove_dead_stream(stream_id, global_stream_id);
7841 return result;
7842 }
7843 // Stream not in map (already closed) — treat as glitch
7844 self.flood_detector.record_glitch();
7845 check_flood_or_return!(self);
7846 self.attribute_bytes_to_overhead();
7847 return MuxResult::Continue;
7848 }
7849 }
7850
7851 // The parser masks the reserved bit (STREAM_ID_MASK), so increment <=
7852 // 2^31-1 and try_from always succeeds. Use try_from rather than `as` to
7853 // guard against a future parser change that drops the mask.
7854 let increment = i32::try_from(increment).unwrap_or(i32::MAX);
7855 // RFC 9113 §6.9: a non-zero WINDOW_UPDATE increment is in [1, 2^31-1].
7856 // Zero was short-circuited above; this asserts the masked value is a
7857 // legal positive increment before we add it to a window.
7858 debug_assert!(
7859 increment > 0,
7860 "WINDOW_UPDATE increment must be strictly positive at this point (zero handled above)"
7861 );
7862 if stream_id == 0 {
7863 // Count connection-level WINDOW_UPDATEs before touching the window
7864 // so a per-window flood stops us before we pay the arithmetic cost
7865 // on a million-frame burst. Zero-increment frames short-circuited
7866 // above, so every increment here is a legal-looking rate consumer.
7867 self.flood_detector.record_window_update_stream0();
7868 check_flood_or_return!(self);
7869 self.attribute_bytes_to_overhead();
7870 // Window arithmetic + its replenish-invariant asserts live on
7871 // `H2FlowControl` now (see its module doc); this arm keeps the
7872 // frame-dispatch orchestration — flood accounting, logging,
7873 // GOAWAY — which needs `self` (flood_detector, readiness,
7874 // log_context!) that the flow-control module deliberately does
7875 // not have.
7876 match self.flow_control.apply_window_update(increment) {
7877 h2_flow_control::ApplyWindowUpdateOutcome::Applied {
7878 new_window,
7879 should_arm_writable,
7880 } => {
7881 if should_arm_writable {
7882 self.readiness.arm_writable();
7883 }
7884 debug!(
7885 "{} WINDOW_UPDATE received: stream=0 increment={} new_connection_window={}",
7886 log_context!(self),
7887 increment,
7888 new_window
7889 );
7890 }
7891 h2_flow_control::ApplyWindowUpdateOutcome::Overflow => {
7892 error!("{} INVALID WINDOW INCREMENT", log_context!(self));
7893 return self.goaway(H2Error::FlowControlError);
7894 }
7895 }
7896 } else if let Some(global_stream_id) = self.stream_table.get(stream_id) {
7897 let stream = &mut context.streams[global_stream_id];
7898 self.attribute_bytes_to_stream(&mut stream.metrics);
7899 // RFC 9113 §6.9: only this connection's leg of the stream.
7900 let stream_window = stream.send_window_mut(&self.position);
7901 let stream_window_before = *stream_window;
7902 if let Some(window) = stream_window.checked_add(increment) {
7903 if *stream_window <= 0 && window > 0 {
7904 self.readiness.arm_writable();
7905 }
7906 *stream_window = window;
7907 // Same replenish invariant as the connection window, applied to
7908 // the per-stream send window (RFC 9113 §6.9.1). Overflow past
7909 // 2^31-1 is rejected by `checked_add` and handled as a
7910 // FLOW_CONTROL_ERROR RST_STREAM below.
7911 debug_assert_eq!(
7912 window,
7913 stream_window_before + increment,
7914 "stream window must increase by exactly the increment"
7915 );
7916 debug_assert!(
7917 window > stream_window_before,
7918 "a positive WINDOW_UPDATE must strictly grow the stream window"
7919 );
7920 debug!(
7921 "{} WINDOW_UPDATE received: stream={} increment={} new_stream_window={}",
7922 log_context!(self),
7923 stream_id,
7924 increment,
7925 window
7926 );
7927 } else {
7928 let result = self.reset_stream(
7929 stream_id,
7930 global_stream_id,
7931 context,
7932 endpoint,
7933 H2Error::FlowControlError,
7934 );
7935 self.remove_dead_stream(stream_id, global_stream_id);
7936 return result;
7937 }
7938 } else {
7939 self.attribute_bytes_to_overhead();
7940 trace!(
7941 "{} Ignoring window update on closed stream {}: {}",
7942 log_context!(self),
7943 stream_id,
7944 increment
7945 );
7946 // Pass 3 Low #5: WINDOW_UPDATE on a closed stream is legal
7947 // (RFC 9113 §6.9.1) but has no useful effect, so a peer that
7948 // keeps sending them is wasting our cycles. Count it as a
7949 // glitch so a flood contributes to `check_flood()` and can
7950 // eventually trigger ENHANCE_YOUR_CALM.
7951 self.flood_detector.record_glitch();
7952 check_flood_or_return!(self);
7953 }
7954 MuxResult::Continue
7955 }
7956
7957 /// Apply a peer's `SETTINGS_INITIAL_WINDOW_SIZE` to every stream window
7958 /// this connection holds (RFC 9113 §6.9.2). `true` when the value is
7959 /// invalid — above 2^31-1, or a change that would push some stream
7960 /// window past it — which the caller answers with
7961 /// GOAWAY(FLOW_CONTROL_ERROR). Every window is checked before any is
7962 /// changed, so a rejected value leaves all of them, and the recorded
7963 /// setting, as they were.
7964 fn update_initial_window_size<L>(&mut self, value: u32, context: &mut Context<L>) -> bool
7965 where
7966 L: ListenerHandler + L7ListenerHandler,
7967 {
7968 if value > FLOW_CONTROL_MAX_WINDOW {
7969 return true;
7970 }
7971 let delta = match i32::try_from(
7972 value as i64 - self.peer_settings.settings_initial_window_size as i64,
7973 ) {
7974 Ok(d) => d,
7975 Err(_) => {
7976 error!("{} initial window size delta overflow", log_context!(self));
7977 return true;
7978 }
7979 };
7980 // RFC 9113 §6.9.2: changes to SETTINGS_INITIAL_WINDOW_SIZE can cause
7981 // stream windows to exceed 2^31-1, which is a flow control error.
7982 // Checked for every stream first, so a rejection changes nothing.
7983 // Only streams owned by this connection, and only this connection's
7984 // leg of each (RFC 9113 §6.9): the other connection's window is its
7985 // own.
7986 let overflows = self
7987 .stream_table
7988 .streams()
7989 .values()
7990 .any(|&global_stream_id| {
7991 context.streams[global_stream_id]
7992 .send_window_mut(&self.position)
7993 .checked_add(delta)
7994 .is_none()
7995 });
7996 if overflows {
7997 return true;
7998 }
7999 let mut open_window = false;
8000 for &global_stream_id in self.stream_table.streams().values() {
8001 let stream_window = context.streams[global_stream_id].send_window_mut(&self.position);
8002 let new_window = *stream_window + delta;
8003 open_window |= *stream_window <= 0 && new_window > 0;
8004 *stream_window = new_window;
8005 }
8006 trace!(
8007 "{} UPDATE INIT WINDOW: {} {} {:?}",
8008 log_context!(self),
8009 delta,
8010 open_window,
8011 self.readiness
8012 );
8013 if open_window {
8014 self.readiness.arm_writable();
8015 }
8016 self.peer_settings.settings_initial_window_size = value;
8017 false
8018 }
8019
8020 /// Take the force-disconnect decision as far as it goes without a socket,
8021 /// and park the rest.
8022 ///
8023 /// Everything this function can settle by itself, it settles here:
8024 /// `H2State::Error`, and on a `Position::Client` connection the backend
8025 /// status and the `HUP` readiness event. What it cannot settle is the
8026 /// `tls_wants_write` answer `h2_close::force_disconnect_action` decides
8027 /// on, because the socket belongs to the shell — so it raises an
8028 /// [`H2ForceDisconnectTarget`] and answers provisionally.
8029 ///
8030 /// The provisional answer is exact everywhere but one case.
8031 /// `Position::Client` always continued. A `Position::Server` whose peer is
8032 /// gone always closes, because `h2_close::force_disconnect_action`
8033 /// answers `CloseSession` on `peer_gone` whatever the socket says. Only a
8034 /// server connection with a live peer genuinely turns on the query, and it
8035 /// answers [`MuxResult::Continue`] until the shell settles it — the
8036 /// conservative direction of the two, since a session held one pass too
8037 /// long is recoverable and a session closed with records still buffered is
8038 /// the truncation `h2_close::force_disconnect_action` exists to prevent.
8039 ///
8040 /// Seventeen sites reach this function and every one returns its answer
8041 /// immediately, so on the ordinary paths no core code runs between the
8042 /// raise and the return of the entry point the shell called.
8043 /// [`Self::goaway`] and `Self::send_initial_goaway` are the exception on
8044 /// their GOAWAY-serialisation failure arms, which `Self::enqueue_rst`'s
8045 /// flood escalation can reach from deep in a frame handler; what those
8046 /// paths can observe in the gap is documented on
8047 /// [`Self::force_disconnect_after_query`].
8048 pub fn force_disconnect(&mut self) -> MuxResult {
8049 self.state = H2State::Error;
8050 // Read ONCE above the `match`, for the borrow reason the
8051 // `tls_wants_write` read that used to sit here had: the client arm
8052 // holds `&mut self.position` for its `status` binding, while this
8053 // query borrows all of `self`. Hoisting is behaviour-neutral — it has
8054 // no side effect and the only field it reads that this function writes
8055 // is `self.state`, assigned above — and it is what lets the raised
8056 // target CARRY the answer instead of re-deriving it later. That
8057 // matters: `peer_gone_after_final_goaway` reads `stream_table` and
8058 // `zero.storage`, and `Self::remove_dead_stream` runs between the
8059 // raise and the settlement on the `Self::reset_stream` paths.
8060 let peer_gone = self.peer_gone_after_final_goaway();
8061 match &mut self.position {
8062 Position::Client(_, _, status) => {
8063 *status = BackendStatus::Disconnecting;
8064 self.readiness.event = Ready::HUP;
8065 self.pending_force_disconnect = Some(H2ForceDisconnectTarget::Client);
8066 MuxResult::Continue
8067 }
8068 Position::Server => {
8069 self.pending_force_disconnect = Some(H2ForceDisconnectTarget::Server { peer_gone });
8070 if peer_gone {
8071 MuxResult::CloseSession
8072 } else {
8073 MuxResult::Continue
8074 }
8075 }
8076 }
8077 }
8078
8079 /// Take the [`H2ForceDisconnectTarget`] this connection parked, if it
8080 /// parked one.
8081 ///
8082 /// Taking rather than reading is what makes a second settlement
8083 /// impossible: the next call sees `None` and the shell's helper answers
8084 /// with the result it was already holding.
8085 pub fn poll_force_disconnect(&mut self) -> Option<H2ForceDisconnectTarget> {
8086 self.pending_force_disconnect.take()
8087 }
8088
8089 /// Settle a [`Self::force_disconnect`] with the `tls_wants_write` answer
8090 /// the shell read on its behalf.
8091 ///
8092 /// `tls_wants_write` arrives as this function's own parameter and the
8093 /// target as the other, which is the same discipline
8094 /// [`H2WritableStateTarget::Flush`] keeps by carrying no `bool`: the
8095 /// pre-raise state and the post-raise socket answer are parameters of one
8096 /// function that never read each other's source, and no binding survives
8097 /// in the raising scope for a later edit to point at the wrong question.
8098 /// The query is read fresh here, exactly as [`Self::force_disconnect`]
8099 /// used to read it fresh at all seventeen sites — including the four whose
8100 /// caller already held an answer, which is why none of them passes its own.
8101 ///
8102 /// Between the raise and this call the core cannot change the answer: it
8103 /// holds no socket, so nothing it runs can flush a record. What the gap
8104 /// can change is what the log lines render. On the ordinary paths there is
8105 /// no gap. On [`Self::goaway`]'s serialisation-failure arm, reachable from
8106 /// `Self::enqueue_rst`, `ConnectionH2::remove_dead_stream` and the rest
8107 /// of a frame handler may run first, so `streams=` and `expect_write=`
8108 /// report the connection as the settlement finds it rather than as the
8109 /// raise left it. `peer_gone` is not among them — it is carried.
8110 pub fn force_disconnect_after_query(
8111 &mut self,
8112 target: H2ForceDisconnectTarget,
8113 tls_wants_write: bool,
8114 ) -> MuxResult {
8115 match target {
8116 H2ForceDisconnectTarget::Client => {
8117 debug!(
8118 "{} H2 force_disconnect client: state={:?}, streams={}, expect_write={:?}, wants_write={}, readiness={:?}",
8119 log_context!(self),
8120 self.state,
8121 self.stream_table.streams().len(),
8122 self.stream_table.expect_write(),
8123 tls_wants_write,
8124 self.readiness
8125 );
8126 MuxResult::Continue
8127 }
8128 // Don't disconnect immediately if rustls still has buffered TLS
8129 // records. Returning CloseSession here triggers shutdown(Write)
8130 // which sends FIN — but any TLS records still in rustls's buffer
8131 // (not yet flushed to the TCP send buffer) are lost, causing the
8132 // client to see "TLS decode error / unexpected eof".
8133 // Instead, keep WRITABLE interest and let the writable path flush.
8134 // The decision itself is `h2_close::force_disconnect_action`,
8135 // exhaustively unit-tested there.
8136 //
8137 // The answer is this function's parameter, and reported by every
8138 // log line here. A literal `wants_write=` in any arm is a second
8139 // copy of a fact the socket already owns, and the closing arm is
8140 // reached with records still pending whenever the peer is gone —
8141 // an operator diagnosing a truncation under HAProxy chaining would
8142 // read the opposite of the socket's state.
8143 H2ForceDisconnectTarget::Server { peer_gone } => {
8144 if h2_close::force_disconnect_action(peer_gone, tls_wants_write)
8145 == CloseAction::ReArmAndContinue
8146 {
8147 debug!(
8148 "{} H2 force_disconnect delaying close: state={:?}, streams={}, expect_write={:?}, wants_write={}, readiness={:?}",
8149 log_context!(self),
8150 self.state,
8151 self.stream_table.streams().len(),
8152 self.stream_table.expect_write(),
8153 tls_wants_write,
8154 self.readiness
8155 );
8156 self.readiness.interest = Ready::WRITABLE | Ready::HUP | Ready::ERROR;
8157 self.ensure_tls_flushed(tls_wants_write);
8158 MuxResult::Continue
8159 } else {
8160 debug!(
8161 "{} H2 force_disconnect closing session: state={:?}, streams={}, expect_write={:?}, wants_write={}, readiness={:?}",
8162 log_context!(self),
8163 self.state,
8164 self.stream_table.streams().len(),
8165 self.stream_table.expect_write(),
8166 tls_wants_write,
8167 self.readiness
8168 );
8169 MuxResult::CloseSession
8170 }
8171 }
8172 }
8173 }
8174
8175 /// Reset a stream: tear down kawa state, emit `RST_STREAM` on the wire,
8176 /// and record MadeYouReset accounting.
8177 ///
8178 /// `wire_stream_id` is the on-wire `StreamId`; `stream_id` is the internal
8179 /// `GlobalStreamId` slot. Callers already carry both so we pass them
8180 /// explicitly rather than scanning `self.streams`. The wire id is threaded
8181 /// into `Self::enqueue_rst` which queues the frame for serialisation in
8182 /// `Self::flush_pending_control_frames` on the next writable tick —
8183 /// independent of whether the caller immediately evicts the slot via
8184 /// `remove_dead_stream` (which they usually do). This is what guarantees
8185 /// the RST reaches the peer for malformed HEADERS / flow-control /
8186 /// content-length violations flagged by h2spec 2.0.
8187 pub fn reset_stream<E, L>(
8188 &mut self,
8189 wire_stream_id: StreamId,
8190 stream_id: GlobalStreamId,
8191 context: &mut Context<L>,
8192 mut endpoint: E,
8193 error: H2Error,
8194 ) -> MuxResult
8195 where
8196 E: Endpoint,
8197 L: ListenerHandler + L7ListenerHandler,
8198 {
8199 // Compute totals before taking mutable borrows on the target stream.
8200 let reset_byte_totals = self.compute_stream_byte_totals(context);
8201 context.unlink_stream(stream_id);
8202 let stream = &mut context.streams[stream_id];
8203 trace!(
8204 "{} reset H2 stream {}: {:#?}",
8205 log_context!(self),
8206 stream_id,
8207 stream.context
8208 );
8209 let old_state = std::mem::replace(&mut stream.state, StreamState::Unlinked);
8210 forcefully_terminate_answer(stream, &mut self.readiness, error);
8211 let linked_token = if let StreamState::Linked(token) = old_state {
8212 Some(token)
8213 } else {
8214 None
8215 };
8216 let (client_rtt, server_rtt) = self.snapshot_rtts(&endpoint, linked_token);
8217 if let Some(token) = linked_token {
8218 endpoint.end_stream(token, stream_id, context);
8219 }
8220 // Emit access log for server-side resets on streams that had active requests
8221 if self.position.is_server()
8222 && matches!(old_state, StreamState::Link | StreamState::Linked(_))
8223 {
8224 let stream = &mut context.streams[stream_id];
8225 self.distribute_overhead(&mut stream.metrics, reset_byte_totals);
8226 stream.metrics.backend_stop();
8227 let events = stream.generate_access_log(
8228 true,
8229 Some("H2::Reset"),
8230 context.listener.clone(),
8231 client_rtt,
8232 server_rtt,
8233 );
8234 self.metric_events.extend(events.into_iter().flatten());
8235 stream.metrics.reset();
8236 }
8237 // Queue the RST for wire emission. Independent of the owning stream
8238 // remaining in `self.streams` — callers typically follow this with
8239 // `remove_dead_stream`, which would otherwise evict the slot before
8240 // `write_streams` could run `kawa.prepare` against the converter.
8241 //
8242 // `enqueue_rst` performs every accounting side-effect at queue
8243 // time (per-error counter, global tx counter, CVE-2025-8671
8244 // MadeYouReset cap). Every caller of `reset_stream` answers a frame
8245 // the peer sent (Content-Length mismatch, header parse error,
8246 // PRIORITY self-dependency, zero-increment or overflowing
8247 // WINDOW_UPDATE), so the reset is `RstOrigin::PeerProvoked`; a
8248 // `NoError` reset is still exempt inside the accounting helper.
8249 if let Some(result) = self.enqueue_rst(wire_stream_id, error, RstOrigin::PeerProvoked) {
8250 return result;
8251 }
8252 MuxResult::Continue
8253 }
8254
8255 pub fn end_stream<L>(&mut self, stream_gid: GlobalStreamId, context: &mut Context<L>)
8256 where
8257 L: ListenerHandler + L7ListenerHandler,
8258 {
8259 context.unlink_stream(stream_gid);
8260 let stream_context = context.http_context(stream_gid);
8261 trace!(
8262 "{} end H2 stream {}: {:#?}",
8263 log_context!(self),
8264 stream_gid,
8265 stream_context
8266 );
8267 match self.position {
8268 Position::Client(..) => {
8269 // Resolve the wire StreamId for this gid up front so the
8270 // subsequent cleanup does not hold an iterator borrow on
8271 // `self.streams` while also mutating it.
8272 let wire_stream_id = self
8273 .stream_table
8274 .streams()
8275 .iter()
8276 .find_map(|(&sid, &gid)| (gid == stream_gid).then_some(sid));
8277 if let Some(id) = wire_stream_id {
8278 // Only send RST_STREAM if the stream hasn't fully completed.
8279 // If both request and response are terminated, the stream is
8280 // already in "closed" state (RFC 9113 §5.1) — sending RST_STREAM
8281 // on a closed stream would be a protocol error that could cause
8282 // the H2 peer to close the entire connection.
8283 //
8284 // Nor for a stream the backend has never seen: its id is
8285 // allocated by `start_stream` when the request is linked,
8286 // but its HEADERS leave with a later write pass, and they
8287 // may never have (the preface is not out yet, the pass has
8288 // not run, or the socket took nothing and the block sits
8289 // parked in `front.out`). On `Position::Client`,
8290 // `front.consumed` is exactly "a request byte reached this
8291 // socket", and a stream's first frame is its HEADERS. A
8292 // RST_STREAM on an idle stream is a connection error
8293 // (RFC 9113 §5.1, §6.4), and one queued ahead of the
8294 // preface breaks it (§3.4): either takes down every stream
8295 // this shared connection carries (sozu-proxy/sozu#1631).
8296 // The stream is forgotten instead, as hyperium/h2 drops a
8297 // stream still pending open: its id is burnt, which the
8298 // next HEADERS on a higher id closes implicitly
8299 // (RFC 9113 §5.1.1). A block parked unsent is dropped with
8300 // it and resets the encoder's table on the next pass
8301 // (`ConnectionH2::parked_header_block`).
8302 //
8303 // "Fully completed" means END_STREAM went both ways: the
8304 // response's was read, and the request's was handed to
8305 // the output, which flushes in order. A request received
8306 // whole is not enough: a backend may answer early, before
8307 // the rest of the body reached it (RFC 9113 §8.1), and a
8308 // stream forgotten with its tail unsent stays open on the
8309 // backend, holding one of its MAX_CONCURRENT_STREAMS
8310 // slots (§5.1.2) for this connection's lifetime.
8311 let stream = &context.streams[stream_gid];
8312 let fully_completed = stream.back_received_end_of_stream
8313 && stream.front.is_terminated()
8314 && stream.front.is_completed();
8315 let opened_on_the_wire = stream.front.consumed;
8316 #[cfg(debug_assertions)]
8317 let queued_before = self.output.len();
8318 if opened_on_the_wire
8319 && !fully_completed
8320 && !self.stream_table.rst_sent_contains(id)
8321 {
8322 // Same path as a PING or SETTINGS ACK: queued whole
8323 // behind any output already waiting, including the
8324 // rest of a stream frame a partial write cut, and
8325 // flushed by the next write pass (#1597, #1604).
8326 if self
8327 .output
8328 .push_frames(RST_STREAM_FRAME_SIZE, |buf| {
8329 serializer::gen_rst_stream(buf, id, H2Error::Cancel)
8330 .map(|(_, size)| size)
8331 })
8332 .is_ok()
8333 {
8334 self.metric_events
8335 .push(MetricEvent::RstStreamSent(H2Error::Cancel));
8336 self.queue_output();
8337 self.stream_table.rst_sent_mut().insert(id);
8338 }
8339 }
8340 // Retire the stream and invalidate expect_write/expect_read
8341 // if they still reference this gid — the slot may be popped
8342 // by `shrink_trailing_recycle` on the next create_stream.
8343 self.remove_dead_stream(id, stream_gid);
8344 // Post: a stream that never opened on the wire leaves no
8345 // frame of its own behind (sozu-proxy/sozu#1631).
8346 #[cfg(debug_assertions)]
8347 debug_assert!(
8348 opened_on_the_wire || self.output.len() == queued_before,
8349 "no frame is queued for a backend stream whose HEADERS never left"
8350 );
8351 debug_assert!(
8352 !self.stream_table.streams().contains_key(&id),
8353 "end_stream must retire the backend stream's wire id"
8354 );
8355 if context.streams[stream_gid].state != StreamState::Recycle {
8356 context.streams[stream_gid].state = StreamState::Unlinked;
8357 }
8358 return;
8359 }
8360 error!(
8361 "{} end_stream called for unknown global_stream_id {}",
8362 log_context!(self),
8363 stream_gid
8364 );
8365 }
8366 Position::Server => {
8367 let stream = &mut context.streams[stream_gid];
8368 let answers_rc = stream.answers.clone();
8369 match end_stream_decision(stream) {
8370 EndStreamAction::ForwardTerminated => {
8371 #[cfg(debug_assertions)]
8372 context
8373 .debug
8374 .push(DebugEvent::Str(format!("Close terminated {stream_gid}")));
8375 debug!(
8376 "{} CLOSING H2 TERMINATED STREAM {} {:?}",
8377 log_context!(self),
8378 stream_gid,
8379 stream
8380 );
8381 stream.state = StreamState::Unlinked;
8382 self.readiness.arm_writable();
8383 context.debug.set_interesting(true);
8384 }
8385 EndStreamAction::CloseDelimited
8386 if stream.back.body_size != kawa::BodySize::Empty =>
8387 {
8388 // A `Content-Length` body short of its length, or a chunked body cut before
8389 // its `0\r\n\r\n` or in its trailers, is truncated (RFC 9112 §6.3, §7.1,
8390 // sozu-proxy/sozu#1633): RST_STREAM, never END_STREAM, as does
8391 // `ConnectionH1::terminate_close_delimited`. The blocks not yet encoded go
8392 // with it, among them a trailer block still waiting for
8393 // its closing flags, which the converter would otherwise
8394 // wait on forever; none of them touched the HPACK table
8395 // (sozu-proxy/sozu#1627).
8396 warn!(
8397 "{} H1 backend closed a {:?} response mid-body on H2 stream {}: emitting RST_STREAM",
8398 log_context!(self),
8399 stream.back.body_size,
8400 stream_gid
8401 );
8402 incr!(names::h1::BACKEND_EOF_BEFORE_MESSAGE_COMPLETE);
8403 stream.back.blocks.clear();
8404 stream
8405 .back
8406 .parsing_phase
8407 .error(kawa::ParsingErrorKind::Processing {
8408 message: "INTERNAL_ERROR",
8409 });
8410 stream.state = StreamState::Unlinked;
8411 self.readiness.arm_writable();
8412 context.debug.set_interesting(true);
8413 debug_assert!(
8414 stream.back.is_error() && stream.back.blocks.is_empty(),
8415 "a truncated response ends in Error with nothing left to encode"
8416 );
8417 }
8418 EndStreamAction::CloseDelimited => {
8419 debug!(
8420 "{} CLOSE DELIMITED H2 STREAM {} {:?}",
8421 log_context!(self),
8422 stream_gid,
8423 stream
8424 );
8425 stream.back.push_block(kawa::Block::Flags(kawa::Flags {
8426 end_body: true,
8427 end_chunk: false,
8428 end_header: false,
8429 end_stream: true,
8430 }));
8431 stream.back.parsing_phase = kawa::ParsingPhase::Terminated;
8432 stream.state = StreamState::Unlinked;
8433 self.readiness.arm_writable();
8434 context.debug.set_interesting(true);
8435 }
8436 EndStreamAction::ForwardUnterminated => {
8437 #[cfg(debug_assertions)]
8438 context
8439 .debug
8440 .push(DebugEvent::Str(format!("Close unterminated {stream_gid}")));
8441 debug!(
8442 "{} CLOSING H2 UNTERMINATED STREAM {} {:?}",
8443 log_context!(self),
8444 stream_gid,
8445 stream
8446 );
8447 forcefully_terminate_answer(
8448 stream,
8449 &mut self.readiness,
8450 H2Error::InternalError,
8451 );
8452 context.debug.set_interesting(true);
8453 }
8454 EndStreamAction::SendDefault(status) => {
8455 #[cfg(debug_assertions)]
8456 context.debug.push(DebugEvent::Str(format!(
8457 "Can't retry, send {status} on {stream_gid}"
8458 )));
8459 let answers = answers_rc.borrow();
8460 set_default_answer(stream, &mut self.readiness, status, &answers);
8461 }
8462 EndStreamAction::ReplayOnFreshBackend => {
8463 // Reachable only through an H1 BACKEND behind this H2
8464 // frontend: `reused_from_pool` — and so the captured
8465 // request — is set by `ConnectionH1::start_stream`
8466 // alone, and an H2 backend never fills it. The action
8467 // is handled here rather than merged into
8468 // `SendDefault` so the H2 frontend re-routes the same
8469 // stale-upstream race the H1 frontend does, with the
8470 // same bytes on the wire.
8471 match stream.queue_upstream_replay() {
8472 Some(len) => {
8473 debug!(
8474 "{} H2 REPLAY {} request bytes on a fresh backend",
8475 log_context!(self),
8476 len
8477 );
8478 // Deliberately still a metric macro, and the
8479 // only one left in this core. It is the one
8480 // labelled site: carrying its cluster and
8481 // backend labels in a `MetricEvent` means
8482 // owned `Option<String>`s, which measures at
8483 // `size_of::<MetricEvent>()` 24 -> 48 bytes —
8484 // paid by every queued event, for a site that
8485 // fires only on a stale-upstream retry — and
8486 // costs the enum its `Copy`. Recording it here
8487 // is the cheaper half of that trade, not an
8488 // oversight.
8489 incr!(
8490 names::backend::RETRY_STALE_UPSTREAM,
8491 stream.context.cluster_id.as_deref(),
8492 stream.context.backend_id.as_deref(),
8493 stream.context.cluster_metrics_incarnation()
8494 );
8495 stream.state = StreamState::Link;
8496 context.pending_links.push_back(stream_gid);
8497 }
8498 None => {
8499 error!(
8500 "{} replay selected with no captured request",
8501 log_context!(self)
8502 );
8503 let answers = answers_rc.borrow();
8504 set_default_answer(stream, &mut self.readiness, 502, &answers);
8505 }
8506 }
8507 }
8508 EndStreamAction::Reconnect => {
8509 debug!("{} H2 RECONNECT", log_context!(self));
8510 #[cfg(debug_assertions)]
8511 context
8512 .debug
8513 .push(DebugEvent::Str(format!("Retry {stream_gid}")));
8514 stream.state = StreamState::Link;
8515 context.pending_links.push_back(stream_gid);
8516 }
8517 }
8518 }
8519 }
8520 }
8521
8522 pub fn start_stream<L>(&mut self, stream: GlobalStreamId, context: &mut Context<L>) -> bool
8523 where
8524 L: ListenerHandler + L7ListenerHandler,
8525 {
8526 // Entry point: a backend connection reaches this from the router
8527 // without necessarily having had `readable`/`writable` called on it
8528 // this pass, so adopt the mux's snapshot before arming the new
8529 // stream's liveness deadline below.
8530 self.now = context.now;
8531 // RFC 9113 §6.8: reject new streams on a draining connection
8532 if self.drain.draining() {
8533 error!(
8534 "{} Cannot open new stream on draining connection (stream {})",
8535 log_context!(self),
8536 stream
8537 );
8538 return false;
8539 }
8540 // RFC 9113 §5.1.2: respect peer's max concurrent streams limit
8541 if self.stream_table.len() >= self.peer_settings.settings_max_concurrent_streams as usize {
8542 error!(
8543 "{} Cannot open new stream: active={} >= peer max_concurrent_streams={}",
8544 log_context!(self),
8545 self.stream_table.len(),
8546 self.peer_settings.settings_max_concurrent_streams
8547 );
8548 return false;
8549 }
8550 trace!(
8551 "{} start new H2 stream {} {:?}",
8552 log_context!(self),
8553 stream,
8554 self.readiness
8555 );
8556 // sozu-proxy/sozu#1632, defense in depth: a request whose front
8557 // already holds output was prepared for another connection — frames
8558 // with its stream id and HPACK state, or H1 bytes — and its blocks
8559 // are gone, so this connection could neither send nor re-encode it.
8560 // `end_stream_decision` and `handle_goaway_frame` never link such a
8561 // request; this refuses one that a future path would, before an id is
8562 // allocated. The caller answers the client with an error.
8563 if !context.streams[stream].front.out.is_empty() {
8564 error!(
8565 "{} Cannot open stream {}: its request was already encoded for another connection",
8566 log_context!(self),
8567 stream
8568 );
8569 return false;
8570 }
8571 let Some(stream_id) = self.new_stream_id() else {
8572 // Pass 4 Medium #5: the client-initiated stream-ID space
8573 // (31 bits, odd only) is exhausted. The backend is now useless
8574 // for new requests — gracefully drain it. Without this
8575 // transition, the Connection lingers in `Connected` state and
8576 // every subsequent request returns 503 because `start_stream`
8577 // keeps returning false.
8578 //
8579 // The session envelope is hoisted to a local because the
8580 // `match &mut self.position` below holds a mutable borrow on
8581 // `self.position`, and `log_context!(self)` reads that field
8582 // for its `position={...}` slot — calling the macro inside the
8583 // match arms would conflict with the active borrow. The
8584 // bidirectional regression guard in `lib/tests/log_layout.rs`
8585 // (and the matching scanner in `lib/build.rs`) recognises this
8586 // shape by scanning backward as well as forward from each log
8587 // call.
8588 let context = log_context!(self);
8589 match &mut self.position {
8590 Position::Client(cluster_id, backend, status) => {
8591 let backend_addr = backend.address;
8592 let cluster = cluster_id.clone();
8593 info!(
8594 "{} H2 backend stream IDs exhausted (cluster={}, backend={:?}) — draining",
8595 context, cluster, backend_addr
8596 );
8597 *status = BackendStatus::Disconnecting;
8598 }
8599 Position::Server => {
8600 error!(
8601 "{} H2 server stream IDs exhausted — sending graceful GOAWAY",
8602 context
8603 );
8604 }
8605 }
8606 self.graceful_goaway(self.now);
8607 return false;
8608 };
8609 // RFC 9113 §6.9: the stream's send window on this connection starts
8610 // at the backend's own SETTINGS_INITIAL_WINDOW_SIZE (the 65535
8611 // default until its SETTINGS arrive), whatever the client granted on
8612 // the frontend connection and whatever a previous request on the
8613 // slot left.
8614 *context.streams[stream].send_window_mut(&self.position) =
8615 i32::try_from(self.peer_settings.settings_initial_window_size).unwrap_or(i32::MAX);
8616 // Pre: the backend leg of the slot starts fresh, whether it comes
8617 // from `Context::create_stream` or from an H1 frontend's keep-alive
8618 // reset (sozu-proxy/sozu#1781): nothing has been received on it yet,
8619 // or `handle_read` would refuse the response as arriving on a closed
8620 // stream. The frontend leg may legitimately be done already: an H2
8621 // client's request ends with END_STREAM before the backend is
8622 // dialled.
8623 debug_assert!(
8624 !context.streams[stream].back_received_end_of_stream
8625 && context.streams[stream].back_data_received == 0,
8626 "a stream must open on a backend with a fresh backend leg"
8627 );
8628 self.stream_table.register(stream_id, stream, self.now);
8629 // Sōzu opened this stream; the backend peer cannot inflate the count.
8630 self.flood_detector.record_stream_opened();
8631 self.readiness.arm_writable();
8632 true
8633 }
8634}
8635
8636/// A [`ConnectionH2`] and the socket it speaks through.
8637///
8638/// The split issue #1339 Q10 decided, in the shape it decided: the core is
8639/// byte-in / byte-out and knows nothing about sockets, while everything that
8640/// reaches one lives here. `Connection::H2` holds this rather than the core,
8641/// so `Connection<Front: SocketHandler>` keeps its bound and `Router::backends`
8642/// keeps its declared type — the parameter did not leave the mux, it moved off
8643/// the state machine.
8644///
8645/// Both halves sit in this module, so this impl reaches the core's private
8646/// fields exactly as the methods it inherited used to. Nothing was widened to
8647/// `pub` to let the socket keep working; the `pub` surface that did grow
8648/// exists for a driver OUTSIDE this crate — `sim/tests/h2_simulation.rs` is
8649/// the first, and it builds its own shell over the same poll/handle pairs
8650/// this one uses.
8651pub struct H2Shell<Front: SocketHandler> {
8652 /// The socket-free state machine.
8653 pub core: ConnectionH2,
8654 /// The only socket in the H2 frontend or backend path.
8655 pub socket: Front,
8656 /// `write_streams`' vectored-write scratch, kept for the connection's
8657 /// lifetime so a transmitting pass reuses its capacity instead of
8658 /// allocating it. Empty between two `h2_transmit::gather` /
8659 /// `h2_transmit::confirm` brackets; private so that bracket cannot be
8660 /// split from outside this module.
8661 io_slices: Vec<IoSlice<'static>>,
8662 /// This connection's one sample of [`Self::socket`]'s round-trip time:
8663 /// `None` until the first stream logged on the connection asks for it
8664 /// through [`ShellEndpoint::local_rtt`] (a frontend's `client_rtt`) or
8665 /// `Connection::rtt` (a backend's `server_rtt`), then `Some(sample)` for
8666 /// the rest of the connection's life — see
8667 /// [`memoized_rtt`](super::memoized_rtt). A `Cell` because
8668 /// `Endpoint::local_rtt` takes `&self`.
8669 pub(super) local_rtt: Cell<Option<Option<Duration>>>,
8670 /// A write to the client failed (reset, broken pipe or another socket
8671 /// error): nothing more can be flushed to it, so the connection reports
8672 /// no pending write and `Self::writable` closes the session instead of
8673 /// waiting for a flush that cannot happen (sozu-proxy/sozu#1792).
8674 write_failed: bool,
8675}
8676
8677/// Renders the core and nothing else.
8678///
8679/// Deliberately NOT derived. A derive would render `socket` through
8680/// `Front`'s own `Debug`, which for a `mio::net::TcpStream` is a local
8681/// address, a peer address and a file descriptor — precisely what
8682/// [`ConnectionH2`]'s own `Debug` stopped rendering, and for the same reasons
8683/// it gives. Wrapping the core in a new type must not hand that back through
8684/// the wrapper.
8685impl<Front: SocketHandler> std::fmt::Debug for H2Shell<Front> {
8686 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
8687 self.core.fmt(f)
8688 }
8689}
8690
8691/// Symmetric tear-down for the four aggregate gauges
8692/// `ConnectionH2::gauge_connection_state` feeds — the three
8693/// `h2.connection.*` metrics and `h2.streams.ready_incremental.by_urgency`:
8694/// whatever positive contribution this connection made is subtracted back out
8695/// when the connection is dropped, and the whole event queue is flushed with
8696/// it.
8697///
8698/// Using `Drop` (rather than wiring decrements into every close path —
8699/// `graceful_goaway`, `force_disconnect`, `handle_goaway_frame`, `Mux::close`,
8700/// stream-id exhaustion, panic-unwind) is what guarantees the gauge is
8701/// arithmetically symmetric regardless of which path teardown took. Past
8702/// underflow incidents (commits ff401b54, aadb3fa4) were increment/decrement
8703/// asymmetries. `ff401b54` carried both shapes at once: a `-1` for streams that
8704/// never ran the `+1`, AND a `-1` that ran twice on one stream (100-Continue,
8705/// and an H2 reset followed by close). `aadb3fa4` was the doubled `-1` alone,
8706/// let through by an early return. `Drop` closes both shapes — it is the sole
8707/// decrement site, and taking `last_gauge_snapshot` makes a second call a
8708/// no-op.
8709///
8710/// It sits on the **shell** rather than on [`ConnectionH2`] because
8711/// `record_metric` writes the worker-local `METRICS` aggregator, which is
8712/// exactly the ambient reach the core gave up. RAII is not given up with it:
8713/// the shell owns the core, so the core cannot outlive this, and because
8714/// `H2Shell` now implements `Drop` the compiler additionally refuses to move
8715/// `core` out of it. If this impl were lost, every live connection's
8716/// contribution would leak upward permanently, with no resync and no
8717/// underflow to notice — which is what
8718/// `dropping_a_connection_releases_its_ready_incremental_contribution` pins.
8719/// The [`Endpoint`] the shell hands its core: every call forwards to `inner`,
8720/// except [`Endpoint::local_rtt`], which the shell answers from its own socket.
8721///
8722/// The core asks for the local round-trip time only when it logs a stream
8723/// (`ConnectionH2::snapshot_rtts`), so the `getsockopt(TCP_INFO)` happens then
8724/// and only then, once per connection: the first ask samples and stores into
8725/// `sample`, every later one on the connection reuses it. Built per core
8726/// call from disjoint borrows of the shell's fields, so the core never holds
8727/// the socket. Issue #1590.
8728struct ShellEndpoint<'a, E, Front> {
8729 inner: &'a mut E,
8730 socket: &'a Front,
8731 sample: &'a Cell<Option<Option<Duration>>>,
8732}
8733
8734impl<'a, E, Front> ShellEndpoint<'a, E, Front> {
8735 fn new(
8736 inner: &'a mut E,
8737 socket: &'a Front,
8738 sample: &'a Cell<Option<Option<Duration>>>,
8739 ) -> Self {
8740 ShellEndpoint {
8741 inner,
8742 socket,
8743 sample,
8744 }
8745 }
8746}
8747
8748impl<E: Endpoint, Front: SocketHandler> std::fmt::Debug for ShellEndpoint<'_, E, Front> {
8749 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
8750 f.debug_struct("ShellEndpoint")
8751 .field("inner", &self.inner)
8752 .field("sample", &self.sample.get())
8753 .finish()
8754 }
8755}
8756
8757impl<E: Endpoint, Front: SocketHandler> Endpoint for ShellEndpoint<'_, E, Front> {
8758 fn readiness(&self, token: mio::Token) -> &Readiness {
8759 self.inner.readiness(token)
8760 }
8761 fn readiness_mut(&mut self, token: mio::Token) -> &mut Readiness {
8762 self.inner.readiness_mut(token)
8763 }
8764 fn peer_rtt(&self, token: mio::Token) -> Option<Duration> {
8765 self.inner.peer_rtt(token)
8766 }
8767 fn local_rtt(&self) -> Option<Duration> {
8768 super::memoized_rtt(self.sample, self.socket.socket_ref())
8769 }
8770 fn end_stream<L: ListenerHandler + L7ListenerHandler>(
8771 &mut self,
8772 token: mio::Token,
8773 stream: GlobalStreamId,
8774 context: &mut Context<L>,
8775 ) {
8776 self.inner.end_stream(token, stream, context)
8777 }
8778 fn start_stream<L: ListenerHandler + L7ListenerHandler>(
8779 &mut self,
8780 token: mio::Token,
8781 stream: GlobalStreamId,
8782 context: &mut Context<L>,
8783 ) -> bool {
8784 self.inner.start_stream(token, stream, context)
8785 }
8786}
8787
8788impl<Front: SocketHandler> Drop for H2Shell<Front> {
8789 fn drop(&mut self) {
8790 self.core.release_connection_gauges();
8791 self.record_pending_metrics();
8792 }
8793}
8794
8795impl<Front: SocketHandler> H2Shell<Front> {
8796 /// The TLS layer is holding encrypted records — handshake, alert, session
8797 /// ticket or already-accepted application data — that it must push before
8798 /// this connection can call a pass finished.
8799 ///
8800 /// **Not "the socket is writable".** The name of the underlying
8801 /// [`SocketHandler`] method says socket, and that is what made this look
8802 /// like an I/O question for as long as it was spelled out at every site.
8803 /// It is a question about a buffer that happens to live behind the socket:
8804 /// `FrontRustls` is the only production handler that overrides it, the
8805 /// trait's default answer is `false`, and `Router::backends` is declared
8806 /// `Connection<SessionTcpStream>` whatever the frontend is — so every
8807 /// backend H2 connection already resolves every one of these queries
8808 /// statically to `false`. The same core is monomorphised twice today, once
8809 /// with TLS and once without, which is the evidence that the coupling is
8810 /// incidental rather than structural.
8811 ///
8812 /// This is a seam, not an abbreviation. It is the single reach where
8813 /// `ConnectionH2` asks that question, and the whole of what a byte-in /
8814 /// byte-out core has to receive as an input instead of querying: the shell
8815 /// that owns the socket is the only layer that can answer it, and it is
8816 /// the layer this method's body moves to. Until then the body is the
8817 /// delegation it always was and the answer is bit-identical.
8818 ///
8819 /// The query is free of side effects, so a caller that needs it twice
8820 /// around a [`Self::flush_tls_records`] is asking two genuinely different
8821 /// questions — "does rustls hold records" and "did the kernel take them" —
8822 /// and a caller that needs it twice with nothing in between should bind it
8823 /// once instead. `h2_close::TlsFlushPhase` names the first case;
8824 /// [`ConnectionH2::force_disconnect`] is the second.
8825 fn tls_wants_write(&self) -> bool {
8826 self.socket.socket_wants_write()
8827 }
8828
8829 /// Ask the TLS layer to push whatever it has buffered towards the kernel,
8830 /// offering it no new application bytes.
8831 ///
8832 /// The empty slice is the whole point: this is a flush, not a write. It is
8833 /// the only *action* among the three TLS seams, which is why the four call
8834 /// sites are the four places where a byte-in / byte-out core must hand
8835 /// control back to its shell rather than take an input — the answer to
8836 /// [`Self::tls_wants_write`] changes across this call, so no value captured
8837 /// before it can stand in for the query after it.
8838 ///
8839 /// Three of the four have made that move and all three are in the shell
8840 /// impl: [`Self::write_streams`] performs it for
8841 /// [`H2FinalizeTarget::Flush`], outside the [`ConnectionH2::finalize_write`] that
8842 /// used to run it inline, and [`Self::writable`] performs both of the
8843 /// connection-level write pass's — its unconditional preamble, and the
8844 /// second one [`H2WritableStateTarget::Flush`] asks for on behalf of the
8845 /// `H2State::GoAway` arm now in [`ConnectionH2::dispatch_writable_state`]. The
8846 /// fourth is [`Self::flush_output_buffer`].
8847 ///
8848 /// Returns the handler's `(size, SocketResult)` unchanged. Three of the
8849 /// four callers discard both and re-query [`Self::tls_wants_write`]
8850 /// instead — deliberately, since a flush that moved bytes may still leave
8851 /// records behind — and only [`Self::flush_output_buffer`] consumes the
8852 /// status, through `update_readiness_after_write`.
8853 fn flush_tls_records(&mut self) -> (usize, SocketResult) {
8854 let (size, status) = self.socket.socket_write(&[]);
8855 self.core.note_write_status(status);
8856 self.record_write(status);
8857 (size, status)
8858 }
8859
8860 /// Remember a failed write to the client: an H2 frontend whose socket
8861 /// answered `Error` or `Closed` can flush nothing more. A backend keeps
8862 /// its own failure handling.
8863 fn record_write(&mut self, status: SocketResult) {
8864 if self.core.position.is_server()
8865 && matches!(status, SocketResult::Error | SocketResult::Closed)
8866 {
8867 self.write_failed = true;
8868 }
8869 }
8870
8871 /// Start the TLS `close_notify` handshake, generating the records that
8872 /// [`Self::tls_wants_write`] will then report as pending.
8873 ///
8874 /// Idempotence is the caller's business, not this seam's:
8875 /// [`Self::initiate_close_notify`] gates it on `close_notify_sent` so a
8876 /// second call cannot queue a second alert.
8877 fn begin_tls_close(&mut self) {
8878 self.socket.socket_close()
8879 }
8880
8881 /// Start TLS close_notify on the frontend and keep the session alive until
8882 /// rustls has flushed the generated records.
8883 pub fn initiate_close_notify(&mut self) -> bool {
8884 if !self.core.position.is_server()
8885 || matches!(
8886 self.core.state,
8887 H2State::ClientPreface | H2State::ClientSettings | H2State::ServerSettings
8888 )
8889 {
8890 return false;
8891 }
8892 if !self.core.close_notify_sent {
8893 trace!("{} H2 initiating CLOSE_NOTIFY", log_context!(self.core));
8894 self.begin_tls_close();
8895 self.core.close_notify_sent = true;
8896 }
8897 let tls_wants_write = self.tls_wants_write();
8898 if tls_wants_write {
8899 self.core.readiness.interest = Ready::WRITABLE | Ready::HUP | Ready::ERROR;
8900 self.core.ensure_tls_flushed(tls_wants_write);
8901 true
8902 } else {
8903 false
8904 }
8905 }
8906
8907 /// Returns `true` if there is data queued waiting to be flushed:
8908 /// - The ordered output queue: control frames (GOAWAY, SETTINGS ACK,
8909 /// etc.) and the rest of a stream frame a stalled write cut
8910 /// - A stream parked by a stalled write (`expect_write`)
8911 /// - Encrypted TLS records in rustls's output buffer not yet flushed to TCP
8912 ///
8913 /// The TLS check is critical: `shutting_down()` uses this to prevent
8914 /// premature session close while response DATA is still in rustls's
8915 /// buffer (accepted by `socket_write_vectored` but not yet on the wire).
8916 ///
8917 /// Does NOT check per-stream `back.out`/`back.blocks`; use
8918 /// [`Self::has_pending_write_full`] on paths that must honour
8919 /// LIFECYCLE invariant 16 (e.g. shutdown-drain).
8920 pub fn has_pending_write(&self) -> bool {
8921 !self.write_failed && self.core.has_pending_write_with(self.tls_wants_write())
8922 }
8923
8924 /// Connection-level [`Self::has_pending_write`] extended with a per-stream
8925 /// back-buffer probe (LIFECYCLE §9 invariant 16). Used by shutdown-drain
8926 /// paths that must not close while any open stream still has outbound
8927 /// kawa bytes queued — a voluntary scheduler yield can leave `back.out`
8928 /// or `back.blocks` non-empty without `expect_write` being set.
8929 pub fn has_pending_write_full<L>(&self, context: &Context<L>) -> bool
8930 where
8931 L: ListenerHandler + L7ListenerHandler,
8932 {
8933 !self.write_failed
8934 && (self.has_pending_write()
8935 || any_stream_has_pending_back(self.core.stream_table.streams(), &context.streams))
8936 }
8937
8938 /// Flush the ordered output queue to the socket, counting its own bytes
8939 /// as connection overhead.
8940 ///
8941 /// Returns `true` if the socket stalled (WouldBlock / zero-length write),
8942 /// meaning the caller should stop writing and wait for the next writable event.
8943 /// Returns `false` when the queue has been fully drained.
8944 fn flush_output_to_socket(&mut self) -> bool {
8945 while !self.core.output_pending().is_empty() {
8946 // The final GOAWAY takes the `close_notify` with it: one
8947 // `write_tls` instead of this one and `H2Shell::close`'s.
8948 let (size, status) = if self.core.output_flush_closes_connection() {
8949 let pending = self.core.output_pending().len();
8950 let (size, status) = self
8951 .socket
8952 .socket_write_then_close(self.core.output_pending());
8953 if size == pending {
8954 self.core.close_notify_sent = true;
8955 }
8956 (size, status)
8957 } else {
8958 self.socket.socket_write(self.core.output_pending())
8959 };
8960 self.record_write(status);
8961 #[cfg(debug_assertions)]
8962 trace!(
8963 "{} flush_output_to_socket: written={}, status={:?}, wants_write={}",
8964 log_context!(self.core),
8965 size,
8966 status,
8967 self.tls_wants_write()
8968 );
8969 if self.core.consume_output_flush(size, status) {
8970 return true;
8971 }
8972 }
8973 false
8974 }
8975
8976 /// Directly flush the ordered output queue to the socket without going
8977 /// through the full writable() path. Used during shutdown when the event
8978 /// loop won't deliver new epoll events for this session (edge-triggered).
8979 ///
8980 /// Whatever is queued can go at any time: every entry is a whole frame
8981 /// (or the rest of one a stalled write cut), queued in wire order, and
8982 /// the queue shares nothing with the read side (#1604). A stream parked
8983 /// by a stalled write keeps its park; only its already-started frame
8984 /// lives in the queue.
8985 pub fn flush_output_buffer(&mut self) {
8986 if self.flush_output_to_socket() {
8987 return;
8988 }
8989 if self.tls_wants_write() {
8990 let (_size, status) = self.flush_tls_records();
8991 let _ = update_readiness_after_write(0, status, &mut self.core.readiness);
8992 }
8993 }
8994
8995 pub fn close<E, L>(&mut self, context: &mut Context<L>, mut endpoint: E)
8996 where
8997 E: Endpoint,
8998 L: ListenerHandler + L7ListenerHandler,
8999 {
9000 match self.core.position {
9001 Position::Client(_, _, BackendStatus::KeepAlive) => {
9002 error!(
9003 "{} H2 connections do not use KeepAlive backend status",
9004 log_context!(self.core)
9005 );
9006 return;
9007 }
9008 Position::Client(..) => {}
9009 Position::Server => {
9010 let tls_pending_before = self.tls_wants_write();
9011 if !self.core.stream_table.streams().is_empty()
9012 || tls_pending_before
9013 || self.core.stream_table.expect_write().is_some()
9014 {
9015 debug!(
9016 "{} H2 close with active state: state={:?}, streams={}, expect_write={:?}, wants_write={}, readiness={:?}",
9017 log_context!(self.core),
9018 self.core.state,
9019 self.core.stream_table.streams().len(),
9020 self.core.stream_table.expect_write(),
9021 tls_pending_before,
9022 self.core.readiness
9023 );
9024 for (stream_id, global_stream_id) in self.core.stream_table.streams() {
9025 let stream = &context.streams[*global_stream_id];
9026 debug!(
9027 "{} close stream id={} gid={}: state={:?}, front_eos={}, back_eos={}, front_phase={:?}, back_phase={:?}, front_completed={}, back_completed={}",
9028 log_context!(self.core),
9029 stream_id,
9030 global_stream_id,
9031 stream.state,
9032 stream.front_received_end_of_stream,
9033 stream.back_received_end_of_stream,
9034 stream.front.parsing_phase,
9035 stream.back.parsing_phase,
9036 stream.front.is_completed(),
9037 stream.back.is_completed()
9038 );
9039 }
9040 }
9041 if !self.core.close_notify_sent {
9042 trace!("{} H2 SENDING CLOSE NOTIFY", log_context!(self.core));
9043 }
9044 let (tls_pending_after, drain_rounds) =
9045 drain_tls_close_notify(&mut self.socket, &mut self.core.close_notify_sent);
9046 if tls_pending_after {
9047 // Severity tiering: key on stream-count + close-state, not
9048 // peer-vs-operator. Composes with the send-side `H2Error`
9049 // variant tier in `goaway()` — both rules demote benign
9050 // paths and keep loss-bearing paths loud.
9051 //
9052 // - `streams != 0` -> `error!`: live streams at
9053 // close time, response-byte loss is possible.
9054 // - `streams == 0` AND state in {GoAway, Error}
9055 // -> `warn!`: idle close after
9056 // a GOAWAY exchange (peer-initiated abort or our own
9057 // graceful drain). What's stranded is best-effort
9058 // GOAWAY/close_notify; no application data was queued.
9059 // - `streams == 0` from any other state
9060 // -> `error!`: unexpected
9061 // teardown path (no GOAWAY exchange) — keep loud so
9062 // unknown failure modes surface.
9063 if !self.core.stream_table.streams().is_empty() {
9064 error!(
9065 "{} TLS buffer NOT fully drained on close: \
9066 pending_before={}, pending_after={}, drain_rounds={}, \
9067 state={:?}, streams={}, expect_write={:?}, \
9068 close_notify_sent={}, readiness={:?}",
9069 log_context!(self.core),
9070 tls_pending_before,
9071 tls_pending_after,
9072 drain_rounds,
9073 self.core.state,
9074 self.core.stream_table.streams().len(),
9075 self.core.stream_table.expect_write(),
9076 self.core.close_notify_sent,
9077 self.core.readiness
9078 );
9079 } else if matches!(self.core.state, H2State::GoAway | H2State::Error) {
9080 warn!(
9081 "{} TLS buffer NOT fully drained on close: \
9082 pending_before={}, pending_after={}, drain_rounds={}, \
9083 state={:?}, streams={}, expect_write={:?}, \
9084 close_notify_sent={}, readiness={:?}",
9085 log_context!(self.core),
9086 tls_pending_before,
9087 tls_pending_after,
9088 drain_rounds,
9089 self.core.state,
9090 self.core.stream_table.streams().len(),
9091 self.core.stream_table.expect_write(),
9092 self.core.close_notify_sent,
9093 self.core.readiness
9094 );
9095 } else {
9096 error!(
9097 "{} TLS buffer NOT fully drained on close: \
9098 pending_before={}, pending_after={}, drain_rounds={}, \
9099 state={:?}, streams={}, expect_write={:?}, \
9100 close_notify_sent={}, readiness={:?}",
9101 log_context!(self.core),
9102 tls_pending_before,
9103 tls_pending_after,
9104 drain_rounds,
9105 self.core.state,
9106 self.core.stream_table.streams().len(),
9107 self.core.stream_table.expect_write(),
9108 self.core.close_notify_sent,
9109 self.core.readiness
9110 );
9111 }
9112 }
9113 return;
9114 }
9115 }
9116 // reconnection is handled by the server for each stream separately
9117 for global_stream_id in self.core.stream_table.streams().values() {
9118 trace!(
9119 "{} end stream: {}",
9120 log_context!(self.core),
9121 global_stream_id
9122 );
9123 if let StreamState::Linked(token) = context.streams[*global_stream_id].state {
9124 endpoint.end_stream(token, *global_stream_id, context);
9125 }
9126 }
9127 }
9128
9129 /// Wrap a freshly built core around the socket it will speak through.
9130 ///
9131 /// The `peer_addr()` call is here because this is the layer that has a
9132 /// socket to ask. It is still the one read of it on the connection's whole
9133 /// lifetime — `ConnectionH2::peer_address` documents why the answer is
9134 /// snapshotted rather than re-read — and it now happens one layer out,
9135 /// before the core exists rather than inside its constructor.
9136 #[allow(clippy::too_many_arguments)]
9137 pub fn new(
9138 session_ulid: Ulid,
9139 socket: Front,
9140 position: super::Position,
9141 buffers: &mut dyn super::buffer_source::BufferSource,
9142 flood_config: H2FloodConfig,
9143 connection_config: H2ConnectionConfig,
9144 stream_idle_timeout: std::time::Duration,
9145 graceful_shutdown_deadline: Option<std::time::Duration>,
9146 timeout_duration: std::time::Duration,
9147 expect_read: Option<(H2StreamId, usize)>,
9148 readiness_interest: sozu_command::ready::Ready,
9149 ) -> Option<Self> {
9150 let core = ConnectionH2::new(
9151 session_ulid,
9152 socket.peer_addr(),
9153 position,
9154 buffers,
9155 flood_config,
9156 connection_config,
9157 stream_idle_timeout,
9158 graceful_shutdown_deadline,
9159 timeout_duration,
9160 expect_read,
9161 readiness_interest,
9162 )?;
9163 Some(H2Shell {
9164 core,
9165 socket,
9166 io_slices: Vec::new(),
9167 local_rtt: Cell::new(None),
9168 write_failed: false,
9169 })
9170 }
9171}
9172
9173/// The socket shell of the H2 data path.
9174///
9175/// Every production `self.socket` in this file is in this block and its
9176/// sibling above it, and `ConnectionH2` has none. `H2Shell::readable` and
9177/// `H2Shell::write_streams` are the two byte movers: each is the caller that
9178/// sits between one of the core's two poll/handle pairs —
9179/// [`ConnectionH2::poll_read_target`] with [`ConnectionH2::handle_read`], and
9180/// [`ConnectionH2::poll_write_target`] with [`ConnectionH2::handle_write`].
9181/// All four are `pub`, and so are the buffers they name
9182/// ([`ConnectionH2::read_space`], [`ConnectionH2::write_buffer`]), so a driver
9183/// outside this crate can stand exactly where these two functions stand.
9184/// `sim/tests/h2_simulation.rs` is that driver, and it is what makes the claim
9185/// measurable rather than asserted.
9186///
9187/// `H2Shell::writable` is the third member, and the only socket it touches it
9188/// touches through the three TLS seams — `H2Shell::tls_wants_write`,
9189/// `H2Shell::flush_tls_records` and `H2Shell::begin_tls_close` — whose bodies
9190/// live here too. It drives two more poll/handle pairs:
9191/// [`ConnectionH2::flush_pending_control_frames`] with
9192/// [`ConnectionH2::handle_control_flush`], and
9193/// [`ConnectionH2::dispatch_writable_state`] with
9194/// [`ConnectionH2::dispatch_writable_state_after_flush`]. Neither takes a
9195/// `Front`: the `(H2State, Position)` dispatch and the control-frame walk are
9196/// both drivable without a socket, which is the whole claim.
9197///
9198/// The fourth pair is `ConnectionH2::force_disconnect` with
9199/// [`ConnectionH2::force_disconnect_after_query`], and it is the one that
9200/// cannot ride a return value — see [`H2ForceDisconnectTarget`]. `H2Shell::settled`
9201/// is where it lands, and every `MuxResult` a core step hands this block goes
9202/// through it.
9203///
9204/// # Why `write_streams` stays private, and where the `unsafe` windows are
9205///
9206/// `H2Shell::write_streams` drives the `Vec<IoSlice<'static>>` that brackets
9207/// `h2_transmit::gather` and `h2_transmit::confirm`, and it drives it because
9208/// the loop that opens and closes that bracket lives with it. The vector
9209/// itself is the shell's private `io_slices` field, so its capacity outlives
9210/// the pass while no descriptor does. There are TWO
9211/// `unsafe` blocks on this path and they nest:
9212///
9213/// - the INNER one is `gather`'s `slice::from_raw_parts`, which re-labels
9214/// borrows of `kawa.storage` as `'static`. It opens and closes inside that
9215/// function, exactly as it always did.
9216/// - the OUTER one is the `unsafe { … }` around the `gather` CALL below, which
9217/// exists because `gather` is `pub unsafe fn`: the lifetime obligation is
9218/// stated in its signature and discharged by the caller. It wraps the call
9219/// and nothing else — not the vectored write, not the `confirm`. It is the
9220/// only `unsafe` block in production code in this file.
9221///
9222/// **The window did not widen when `write_streams` moved onto `H2Shell`, nor
9223/// when the vector moved into a field of it.** It still opens at that `gather`
9224/// call and closes at the `confirm` three statements later, where
9225/// `io_slices.clear()` runs before the consume which may relocate the storage.
9226/// The only thing entered while the descriptors are live is
9227/// `SocketHandler::socket_write_vectored`, which reborrows them for the
9228/// duration of the call and cannot retain them. No `IoSlice<'static>`
9229/// survives one iteration of the loop: what the field keeps between passes is
9230/// capacity, and the vector is empty whenever `write_streams` is not inside
9231/// that bracket. `gather` clears it on entry as well, so even a descriptor
9232/// stranded by a panic out of the vectored write is discarded before anything
9233/// could read it. What changed is the
9234/// receiver, from `&mut ConnectionH2<Front>` to `&mut H2Shell<Front>`, and the
9235/// buffer now arrives through [`ConnectionH2::write_buffer`] instead of the
9236/// free function — neither of which moves either end of the bracket.
9237///
9238/// That the bracket does not span the poll/handle seam matters MORE now, not
9239/// less: the seam is `pub`, the buffers it names are `pub`, and an
9240/// out-of-crate driver really does exist, so halves on opposite sides of it
9241/// could be separated by a caller this crate cannot see. `write_streams` stays
9242/// private for that reason, on `H2Shell` exactly as it was on `ConnectionH2`.
9243/// `h2_transmit`'s pair is exported — `gather` as `pub unsafe fn`, `confirm`
9244/// as a plain `pub fn` that produces no descriptor and so leaves no
9245/// obligation — which moves the same argument onto that driver's own bracket
9246/// rather than dissolving it. `H2Harness::write_streams` in
9247/// `sim/tests/h2_simulation.rs` opens and closes its own bracket inside its
9248/// own single function, for the same reason and with the same shape.
9249impl<Front: SocketHandler> H2Shell<Front> {
9250 /// Answer a core step, settling any [`H2ForceDisconnectTarget`] it parked
9251 /// on the way.
9252 ///
9253 /// The one place the socket answers a force-disconnect, and the reason
9254 /// `ConnectionH2::force_disconnect` can be raised from seventeen sites
9255 /// that hold no socket. Every `MuxResult` a core step hands this impl
9256 /// block leaves through here, which is what makes the discipline
9257 /// greppable rather than asserted: a core call in this block whose type is
9258 /// [`MuxResult`] and which does not appear as this function's argument is
9259 /// a missed settlement.
9260 ///
9261 /// `result` is the core's provisional answer and survives untouched when
9262 /// nothing was parked — the overwhelmingly common case, since only a
9263 /// force-disconnect parks anything.
9264 /// [`ConnectionH2::force_disconnect`], settled.
9265 ///
9266 /// The public spelling of the decision: the core raises it and this reads
9267 /// the query it could not. Every out-of-shell caller goes through here.
9268 pub fn force_disconnect(&mut self) -> MuxResult {
9269 let result = self.core.force_disconnect();
9270 self.settled(result)
9271 }
9272
9273 /// [`ConnectionH2::graceful_goaway`], settled.
9274 ///
9275 /// Reaches `ConnectionH2::force_disconnect` through
9276 /// `ConnectionH2::send_initial_goaway`'s serialisation-failure arm.
9277 pub fn graceful_goaway(&mut self, now: Instant) -> MuxResult {
9278 let result = self.core.graceful_goaway(now);
9279 self.settled(result)
9280 }
9281
9282 /// [`ConnectionH2::goaway_before_forced_close`], settled.
9283 ///
9284 /// Reaches `ConnectionH2::force_disconnect` through the
9285 /// serialisation-failure arm of the final GOAWAY.
9286 pub fn goaway_before_forced_close<L>(&mut self, context: &Context<L>) -> MuxResult
9287 where
9288 L: ListenerHandler + L7ListenerHandler,
9289 {
9290 let result = self.core.goaway_before_forced_close(context);
9291 self.settled(result)
9292 }
9293
9294 /// [`ConnectionH2::start_stream`], settled.
9295 ///
9296 /// Answers the core's `bool` unchanged. The settlement is still required
9297 /// and still belongs HERE: the core reaches
9298 /// `ConnectionH2::graceful_goaway` from its stream-id-exhaustion arm, and
9299 /// the readiness that settlement writes is read by `Mux` before it decides
9300 /// whether this connection gets a `writable` at all.
9301 pub fn start_stream<L>(&mut self, stream: GlobalStreamId, context: &mut Context<L>) -> bool
9302 where
9303 L: ListenerHandler + L7ListenerHandler,
9304 {
9305 let started = self.core.start_stream(stream, context);
9306 self.settle();
9307 started
9308 }
9309
9310 /// [`ConnectionH2::cancel_timed_out_streams`], settled.
9311 ///
9312 /// The core answers nothing and already discards the flood-violation
9313 /// `MuxResult` its `ConnectionH2::enqueue_rst` sweep can raise — see its
9314 /// own comment. That discard is preserved: what this adds is the readiness
9315 /// half of a force-disconnect that discard used to leave completed.
9316 pub fn cancel_timed_out_streams<E, L>(&mut self, context: &mut Context<L>, endpoint: &mut E)
9317 where
9318 E: Endpoint,
9319 L: ListenerHandler + L7ListenerHandler,
9320 {
9321 self.core.cancel_timed_out_streams(
9322 context,
9323 &mut ShellEndpoint::new(endpoint, &self.socket, &self.local_rtt),
9324 );
9325 self.settle();
9326 }
9327
9328 /// [`ConnectionH2::end_stream`], forwarded and drained.
9329 ///
9330 /// No settlement: `ConnectionH2::end_stream` cannot reach
9331 /// `ConnectionH2::force_disconnect`. It is here only because
9332 /// `Connection`'s `forward!` needs one name on both arms.
9333 ///
9334 /// It still has to drain. The core's `end_stream` serialises a
9335 /// RST_STREAM(CANCEL) for a stream the peer abandoned, and that is the one
9336 /// producer this file reaches through a public entry point that settles
9337 /// neither through `H2Shell::settled` nor through `H2Shell::settle`.
9338 pub fn end_stream<L>(&mut self, stream_gid: GlobalStreamId, context: &mut Context<L>)
9339 where
9340 L: ListenerHandler + L7ListenerHandler,
9341 {
9342 self.core.end_stream(stream_gid, context);
9343 self.record_pending_metrics();
9344 }
9345
9346 /// Drive [`ConnectionH2::flush_pending_control_frames`] to a terminal
9347 /// answer, performing the output flush it asks for.
9348 ///
9349 /// The loop is the shell's because the socket is: the core walks its
9350 /// stages, queues their frames behind the ordered output, and stops once
9351 /// with the bytes it cannot move, then is told what the write reported.
9352 /// At most two iterations run, and a stall leaves the loop with
9353 /// [`H2ControlFlushTarget::Stalled`], which is the caller's cue to read
9354 /// the TLS query the stalled write just changed the answer to.
9355 fn drive_control_flush(&mut self) -> ControlFlushOutcome {
9356 let mut target = self.core.flush_pending_control_frames();
9357 loop {
9358 match target {
9359 H2ControlFlushTarget::FlushOutput => {
9360 let stalled = self.flush_output_to_socket();
9361 target = self.core.handle_control_flush(stalled);
9362 }
9363 H2ControlFlushTarget::Proceed => return ControlFlushOutcome::Proceed,
9364 H2ControlFlushTarget::Done(result) => return ControlFlushOutcome::Done(result),
9365 H2ControlFlushTarget::Stalled => return ControlFlushOutcome::Stalled,
9366 }
9367 }
9368 }
9369
9370 fn settled(&mut self, result: MuxResult) -> MuxResult {
9371 let settled = match self.core.poll_force_disconnect() {
9372 None => result,
9373 Some(target) => {
9374 let tls_wants_write = self.tls_wants_write();
9375 self.core
9376 .force_disconnect_after_query(target, tls_wants_write)
9377 }
9378 };
9379 self.record_pending_metrics();
9380 settled
9381 }
9382
9383 /// Record every [`MetricEvent`] the core queued during the step that just
9384 /// ended.
9385 ///
9386 /// The shell half of the boundary: the core states the arithmetic, this
9387 /// writes it into the worker-local `METRICS` aggregator.
9388 ///
9389 /// Three sites, and between them they cover every producer:
9390 ///
9391 /// - [`Self::settled`], so after every `readable`, `writable`,
9392 /// `start_stream`, `cancel_timed_out_streams`, `force_disconnect` and
9393 /// `graceful_goaway`, and after `writable_inner`'s
9394 /// [`Self::drive_control_flush`], whose only caller it is;
9395 /// - [`Self::end_stream`], the one public entry point that reaches a
9396 /// producer and settles through neither;
9397 /// - [`Drop`].
9398 ///
9399 /// The remaining public entry points — `close`, `initiate_close_notify`,
9400 /// `flush_output_buffer`, `has_pending_write`, `has_pending_write_full` —
9401 /// reach no producer but one: they touch the TLS layer, the output queue
9402 /// and readiness, and of the core methods they call
9403 /// (`consume_output_flush`, `ensure_tls_flushed`, `has_pending_write_with`,
9404 /// `output_pending`) only `consume_output_flush` queues a metric, the
9405 /// overhead byte count, which the next [`Self::settled`] records.
9406 ///
9407 /// A step that reached none of the three would still not lose an event:
9408 /// the queue is FIFO, so the next drain records the same values, and
9409 /// `Drop` drains whatever is left. That is what bounds the queue, and what
9410 /// keeps "drain here" a scheduling choice rather than a correctness one.
9411 fn record_pending_metrics(&mut self) {
9412 for event in self.core.drain_metric_events() {
9413 record_metric(event);
9414 }
9415 }
9416
9417 /// [`ConnectionH2::settled`] for a core step whose own answer is not a
9418 /// [`MuxResult`], or is one this layer has always dropped.
9419 ///
9420 /// `ConnectionH2::start_stream` answers a `bool` and
9421 /// `ConnectionH2::cancel_timed_out_streams` answers nothing, yet both
9422 /// reach `ConnectionH2::force_disconnect` — the first through
9423 /// `ConnectionH2::graceful_goaway`, the second through
9424 /// `ConnectionH2::enqueue_rst`'s flood escalation, which it already
9425 /// discards with `let _ =`. The settlement still has to happen HERE rather
9426 /// than at the next entry point, because it writes `readiness` and `Mux`
9427 /// reads `Readiness::filter_interest` between entry points to decide
9428 /// whether to call `readable`/`writable` at all.
9429 fn settle(&mut self) {
9430 let _ = self.settled(MuxResult::Continue);
9431 }
9432
9433 /// Drive one frontend read pass.
9434 ///
9435 /// The core lives in `Self::poll_read_target` and `Self::handle_read`,
9436 /// both `pub`; this function is the caller that sits between them, and its
9437 /// `self.socket.socket_read` is the only socket touch on the whole H2 read
9438 /// path. It lives in the shell impl rather than among the core impls for
9439 /// the reason stated there: the socket touch sits where the socket does,
9440 /// and the frame state machine between the two calls is drivable without
9441 /// one, exactly as `h2_transmit::gather`/`confirm` already bracket the
9442 /// vectored write.
9443 pub fn readable<E, L>(&mut self, context: &mut Context<L>, endpoint: E) -> MuxResult
9444 where
9445 E: Endpoint,
9446 L: ListenerHandler + L7ListenerHandler,
9447 {
9448 if self.core.is_lingering() {
9449 // Entry point: adopt the mux's snapshot, as `poll_read_target`
9450 // would. `graceful_shutdown_deadline_elapsed` reads it, and a
9451 // snapshot frozen at the linger's start would keep a soft stop's
9452 // budget from ever elapsing.
9453 self.core.adopt_now(context.now);
9454 return self.drain_linger(context.now);
9455 }
9456 let result = self.readable_inner(context, endpoint);
9457 self.settled(result)
9458 }
9459
9460 /// Turn a close decided by a writable pass that began after the final
9461 /// GOAWAY ([`ConnectionH2::lingers_instead_of_closing`]) into a lingering
9462 /// close, unless the client hung up meanwhile: queue and
9463 /// flush `close_notify` first (a TLS alert must precede the FIN), then
9464 /// shut the write side down, so the FIN follows everything sent, and
9465 /// read what the client still sends until [`Self::drain_linger`] closes.
9466 /// Any other result is returned unchanged.
9467 fn linger_instead_of_closing(&mut self, result: MuxResult) -> MuxResult {
9468 if !matches!(result, MuxResult::CloseSession) || self.core.frontend_hung_up_while_draining()
9469 {
9470 return result;
9471 }
9472 let (duration, max_bytes) = self.core.linger_budget();
9473 let deadline = match self.core.linger {
9474 Linger::Pending { deadline } => deadline,
9475 _ => match self.core.now.checked_add(duration) {
9476 Some(deadline) => deadline,
9477 None => return MuxResult::CloseSession,
9478 },
9479 };
9480 if self.initiate_close_notify() {
9481 // `close_notify` is pending: the next writable pass flushes it
9482 // and closes again, which lands here once nothing is left. The
9483 // state is `Error` by then, so the pending linger is what keeps
9484 // that pass eligible.
9485 self.core.linger = Linger::Pending { deadline };
9486 return MuxResult::Continue;
9487 }
9488 if let Err(e) = super::shutdown_write(self.socket.socket_ref(), false) {
9489 debug!(
9490 "{} H2 closing without lingering after the final GOAWAY: shutdown failed: {:?}",
9491 log_context!(self.core),
9492 e
9493 );
9494 return MuxResult::CloseSession;
9495 }
9496 debug!(
9497 "{} H2 lingering after the final GOAWAY: draining what the client still sends",
9498 log_context!(self.core)
9499 );
9500 self.core.linger = Linger::Draining {
9501 deadline,
9502 remaining: max_bytes,
9503 };
9504 // Fixed, never re-armed: the deadline bounds the whole drain.
9505 self.core.timeout_deadline = Some(deadline);
9506 self.core.readiness.interest = Ready::READABLE | Ready::HUP | Ready::ERROR;
9507 self.core.readiness.event.remove(Ready::WRITABLE);
9508 // Edge-triggered epoll will not report bytes already queued.
9509 self.core.readiness.event.insert(Ready::READABLE);
9510 MuxResult::Continue
9511 }
9512
9513 /// Read and drop what the client still sends while lingering. Closes on
9514 /// the client's EOF, a socket error, the byte budget, or the deadline;
9515 /// otherwise waits for the next READABLE.
9516 fn drain_linger(&mut self, now: Instant) -> MuxResult {
9517 let Linger::Draining {
9518 deadline,
9519 mut remaining,
9520 } = self.core.linger
9521 else {
9522 unreachable!("drain_linger runs only while lingering");
9523 };
9524 if now >= deadline {
9525 debug!(
9526 "{} H2 lingering close: deadline reached",
9527 log_context!(self.core)
9528 );
9529 return MuxResult::CloseSession;
9530 }
9531 let result = match drain_discard(self.socket.socket_ref(), &mut remaining, |size| {
9532 record_metric(MetricEvent::FrontendBytesIn(size as i64));
9533 }) {
9534 LingerRead::Wait => {
9535 self.core.readiness.event.remove(Ready::READABLE);
9536 MuxResult::Continue
9537 }
9538 LingerRead::Close => {
9539 debug!(
9540 "{} H2 lingering close: EOF, error or {} bytes left of the budget",
9541 log_context!(self.core),
9542 remaining
9543 );
9544 MuxResult::CloseSession
9545 }
9546 };
9547 self.core.linger = Linger::Draining {
9548 deadline,
9549 remaining,
9550 };
9551 result
9552 }
9553
9554 fn readable_inner<E, L>(&mut self, context: &mut Context<L>, mut endpoint: E) -> MuxResult
9555 where
9556 E: Endpoint,
9557 L: ListenerHandler + L7ListenerHandler,
9558 {
9559 match self.core.poll_read_target(
9560 context,
9561 &mut ShellEndpoint::new(&mut endpoint, &self.socket, &self.local_rtt),
9562 ) {
9563 H2ReadTarget::Done(result) => result,
9564 H2ReadTarget::Skip(stream_id) => self.core.handle_read(
9565 context,
9566 ShellEndpoint::new(&mut endpoint, &self.socket, &self.local_rtt),
9567 stream_id,
9568 H2ReadOutcome::Skipped,
9569 ),
9570 H2ReadTarget::Fill { stream_id, amount } => {
9571 let space = self.core.read_space(context, stream_id, amount);
9572 let (size, status) = self.socket.socket_read(space);
9573 self.core.handle_read(
9574 context,
9575 ShellEndpoint::new(&mut endpoint, &self.socket, &self.local_rtt),
9576 stream_id,
9577 H2ReadOutcome::Filled {
9578 amount,
9579 size,
9580 status,
9581 },
9582 )
9583 }
9584 }
9585 }
9586
9587 /// Drive one frontend write pass.
9588 ///
9589 /// The core lives in `Self::flush_pending_control_frames`,
9590 /// `Self::dispatch_writable_state` and
9591 /// `Self::dispatch_writable_state_after_flush`; this function is the
9592 /// caller that sits between them, and every TLS seam call this function's
9593 /// own body used to make inline is here. It lives in the shell impl rather
9594 /// than among the core impls for the reason stated there: the socket
9595 /// touches sit where the socket does, and the `(H2State, Position)`
9596 /// dispatch between them is drivable without one.
9597 ///
9598 /// The pass performs the query / flush / query triple `h2_close` names
9599 /// TWICE, and the second time only for one arm:
9600 ///
9601 /// 1. the preamble — read `Self::tls_wants_write`, flush if it says yes.
9602 /// It pushes bytes for every state, which is why it is unconditional
9603 /// rather than owned by a close arm.
9604 /// 2. a FRESH read of the same query, which is that preamble's post-flush
9605 /// answer and the pre-flush question both close arms decide on. It is
9606 /// handed to `Self::dispatch_writable_state` as an input.
9607 /// 3. for `H2WritableStateTarget::Flush` alone — the `H2State::GoAway`
9608 /// arm — `Self::flush_tls_records` again, a third read, and
9609 /// `Self::dispatch_writable_state_after_flush`.
9610 ///
9611 /// Each read is a separate `let`, and the third shadows the second inside
9612 /// the `Flush` arm only. That is the whole guard against the confusion
9613 /// `h2_close::TlsFlushPhase` exists to prevent: "does rustls hold records"
9614 /// and "did the kernel take them" are two questions, the second cannot be
9615 /// read off the first, and a core that reused one answer for both would
9616 /// close a connection with records pending.
9617 ///
9618 /// The tick count is load-bearing: the flush and the re-query happen
9619 /// within THIS call and the pass does not return `Continue` to be called
9620 /// again, which `a_flush_that_succeeds_closes_within_one_writable_call`
9621 /// pins. See `h2_close`'s module doc for why.
9622 pub fn writable<E, L>(&mut self, context: &mut Context<L>, endpoint: E) -> MuxResult
9623 where
9624 E: Endpoint,
9625 L: ListenerHandler + L7ListenerHandler,
9626 {
9627 if self.core.is_lingering() {
9628 // Entry point: adopt the mux's snapshot, as `writable_inner` does.
9629 self.core.adopt_now(context.now);
9630 // The write side is shut down: nothing is left to write.
9631 self.core.readiness.interest.remove(Ready::WRITABLE);
9632 self.core.readiness.event.remove(Ready::WRITABLE);
9633 return MuxResult::Continue;
9634 }
9635 // Asked before the pass: the close it decides moves the state from
9636 // `GoAway` to `Error` (`ConnectionH2::force_disconnect`).
9637 let after_final_goaway = self.core.lingers_instead_of_closing();
9638 let result = self.writable_inner(context, endpoint);
9639 if self.write_failed {
9640 // The client is gone or its socket can never be flushed: close
9641 // at once, as an H1 frontend does (`ConnectionH1::writable`).
9642 debug!(
9643 "{} H2 closing the frontend after a failed write",
9644 log_context!(self.core)
9645 );
9646 return MuxResult::CloseSession;
9647 }
9648 let result = self.settled(result);
9649 if after_final_goaway {
9650 self.linger_instead_of_closing(result)
9651 } else {
9652 result
9653 }
9654 }
9655
9656 fn writable_inner<E, L>(&mut self, context: &mut Context<L>, endpoint: E) -> MuxResult
9657 where
9658 E: Endpoint,
9659 L: ListenerHandler + L7ListenerHandler,
9660 {
9661 // Entry point: adopt the mux's snapshot for this pass.
9662 self.core.adopt_now(context.now);
9663 // Only this pass's writes may withhold its TLS re-arm.
9664 self.core.kernel_refused_write = false;
9665 self.core.prune_inactive_streams_while_closing(context);
9666
9667 match self.drive_control_flush() {
9668 ControlFlushOutcome::Done(result) => return result,
9669 // The stalled-drain tail the stages used to run inline. The
9670 // query belongs here and not there: `flush_output_to_socket` is what
9671 // changed the answer, so it has to be read after the stall, and
9672 // this is the layer that owns the socket.
9673 ControlFlushOutcome::Stalled => {
9674 let tls_wants_write = self.tls_wants_write();
9675 self.core.ensure_tls_flushed(tls_wants_write);
9676 return MuxResult::Continue;
9677 }
9678 ControlFlushOutcome::Proceed => {}
9679 }
9680
9681 // Flush any pending TLS records before state-specific processing.
9682 // This ensures response DATA frames that were accepted by rustls
9683 // (via socket_write_vectored in write_streams) are pushed to the
9684 // TCP socket even when the connection is in GoAway or Error state.
9685 // Without this, the state-specific handlers may call force_disconnect()
9686 // before the response data reaches the kernel's TCP send buffer.
9687 if self.tls_wants_write() {
9688 self.flush_tls_records();
9689 }
9690
9691 // A fresh query, not the binding above: this one asks whether the
9692 // flush between them landed. It is read here rather than inside the
9693 // two close arms that consult it because those arms no longer touch
9694 // the socket, and it is read ONCE because only one of them can run.
9695 let tls_wants_write = self.tls_wants_write();
9696 match self.core.dispatch_writable_state(tls_wants_write) {
9697 H2WritableStateTarget::Done(result) => result,
9698 // The `H2State::GoAway` arm's own flush, the second this pass
9699 // attempts, and the only *action* the close decision cannot take
9700 // for itself.
9701 H2WritableStateTarget::Flush => {
9702 self.flush_tls_records();
9703 // Again a fresh query, and again a different question from
9704 // the one that produced `Flush`: `flush_tls_records`'s
9705 // `(size, status)` is discarded, so this read is the only way
9706 // the pass learns whether the records reached the kernel.
9707 let tls_wants_write = self.tls_wants_write();
9708 self.core
9709 .dispatch_writable_state_after_flush(tls_wants_write)
9710 }
9711 H2WritableStateTarget::WriteStreams => self.write_streams(context, endpoint),
9712 }
9713 }
9714
9715 /// Write application data (request/response bodies, headers) across all
9716 /// active streams, respecting priority ordering and flow control.
9717 ///
9718 /// This is the main data-plane write path: it resumes any partially-written
9719 /// stream, prepares new frames via the H2 block converter, flushes them to
9720 /// the socket, and recycles completed streams.
9721 ///
9722 /// The core lives in [`ConnectionH2::poll_write_target`] and [`ConnectionH2::handle_write`],
9723 /// both `pub`; this function is the caller that sits between them, and its
9724 /// `self.socket.socket_write_vectored` is the only place a stream's bytes
9725 /// reach the socket. It lives in the shell impl rather than among the core
9726 /// impls for the reason stated there, exactly as `readable()` keeps
9727 /// `socket_read` outside [`ConnectionH2::poll_read_target`].
9728 ///
9729 /// The pass's last step is a socket touch too, and for the same reason it
9730 /// sits here: [`H2WriteTarget::Finalize`] is answered by reading
9731 /// [`Self::tls_wants_write`], driving the [`H2FinalizeTarget`]
9732 /// [`ConnectionH2::finalize_write`] answers — performing
9733 /// [`Self::flush_tls_records`] for [`H2FinalizeTarget::Flush`] and not for
9734 /// [`H2FinalizeTarget::SkipFlush`] — and reading the query again for
9735 /// [`ConnectionH2::finalize_write_after_flush`]. That is the query / flush / query
9736 /// triple `h2_close::TlsFlushPhase` names, and the three TLS seams it
9737 /// reaches are what a byte-in / byte-out core receives as inputs instead
9738 /// of asking for.
9739 ///
9740 /// Unlike `readable()` this is a LOOP and not a match: one read pass
9741 /// performs one `socket_read`, while one write pass performs an unbounded
9742 /// number of vectored writes — the pre-image's `while !kawa.out.is_empty()`
9743 /// nested inside its walk of the scheduler's order. Every iteration here is
9744 /// one of those rounds.
9745 ///
9746 /// The `Vec<IoSlice<'static>>` came here WITH the loop, and is only ever
9747 /// filled inside this one private function, on purpose. It is the shell's
9748 /// private `io_slices` field rather than a local so that a transmitting
9749 /// pass reuses its capacity instead of allocating it: the local cost one
9750 /// heap allocation per pass, at least one per response.
9751 /// [`h2_transmit::gather`] is a `pub unsafe fn` that hands back
9752 /// descriptors carrying a lifetime they do
9753 /// not have, and [`h2_transmit::confirm`] must discharge them before the
9754 /// consume that may relocate `kawa.storage`; bracketing the two around the
9755 /// write in three adjacent statements keeps that window exactly as wide as
9756 /// the pre-image's loop body kept it, and stops it from spanning the
9757 /// poll/handle boundary — now a `pub` one, which is why the bracket's
9758 /// locality is load-bearing rather than tidy. The `unsafe { … }` below
9759 /// wraps the `gather` call alone; see the shell impl's own note.
9760 ///
9761 /// The [`converter::H2BlockConverter`] is scoped to a single
9762 /// `kawa.prepare` call rather than to the whole per-stream loop, so the
9763 /// `&mut self.core.hpack` borrow it takes for the connection's HPACK encoder
9764 /// never spans the loop. Every `&self` / `&mut self` method is therefore
9765 /// callable inside the loop body; what remains deferred to after it —
9766 /// RST accounting and stream retirement — is deferred for its own
9767 /// ordering reasons, documented at each site.
9768 /// [`converter::H2ConverterPass`] carries the scratch buffers and the
9769 /// RFC 7541 §6.3 size-update signal from one stream's `prepare` to the
9770 /// next by moving them, so the narrower scope costs no copy.
9771 fn write_streams<E, L>(&mut self, context: &mut Context<L>, mut endpoint: E) -> MuxResult
9772 where
9773 E: Endpoint,
9774 L: ListenerHandler + L7ListenerHandler,
9775 {
9776 // No `arm_timeout()` here. `writable()` reaches this function on every
9777 // proxying-state pass, including one whose only output was the PING or
9778 // SETTINGS acknowledgement `flush_pending_control_frames` drained a few
9779 // statements earlier, so arming at pass entry renewed the session for a
9780 // peer that had sent nothing but keepalives. The write side's share of
9781 // LIFECYCLE §9 invariant 9 now lives in `ConnectionH2::handle_write`,
9782 // where the pass knows a real stream's bytes reached the socket.
9783 // Pre-compute byte totals for proportional overhead distribution.
9784 let mut pass = H2WritePass::new(self.core.compute_stream_byte_totals(context));
9785
9786 loop {
9787 match self.core.poll_write_target(
9788 context,
9789 &mut ShellEndpoint::new(&mut endpoint, &self.socket, &self.local_rtt),
9790 &mut pass,
9791 ) {
9792 H2WriteTarget::Done(result) => return result,
9793 H2WriteTarget::Finalize {
9794 socket_write,
9795 bytes_written,
9796 } => {
9797 let tls_wants_write = self.tls_wants_write();
9798 match self.core.finalize_write(
9799 tls_wants_write,
9800 socket_write,
9801 bytes_written,
9802 context,
9803 ) {
9804 H2FinalizeTarget::Done(result) => return result,
9805 // The only *action* of the three TLS seams, and the
9806 // one step the core cannot take for itself.
9807 H2FinalizeTarget::Flush => {
9808 self.flush_tls_records();
9809 }
9810 // The vectored writes above already attempted this
9811 // pass's flush; a second empty-buffer write would be a
9812 // syscall for nothing.
9813 H2FinalizeTarget::SkipFlush => {}
9814 }
9815 // A fresh query, not the binding above: this one asks
9816 // whether the step between them landed.
9817 let tls_wants_write = self.tls_wants_write();
9818 return self.core.finalize_write_after_flush(tls_wants_write);
9819 }
9820 H2WriteTarget::Transmit { stream_id } => {
9821 // Gather / write / measure / confirm. The gather borrows
9822 // the queued output and `kawa.storage`, and hands back
9823 // descriptors with an extended lifetime; `confirm`
9824 // discharges that obligation before the consume.
9825 //
9826 // SAFETY: neither the output queue nor `kawa` is dropped
9827 // or mutated between this call and the `confirm` below,
9828 // and that `confirm` clears `self.io_slices` before the
9829 // `Kawa::consume` which may relocate `kawa.storage`. The
9830 // only things entered while the descriptors are live are
9831 // the vectored write, which reborrows them for the
9832 // duration of the call and cannot retain them, and
9833 // `frame_tail`, which only reads them.
9834 let (prefix, offered) = unsafe {
9835 self.core
9836 .gather_transmit(context, stream_id, &mut self.io_slices)
9837 };
9838 let (size, status) = self.socket.socket_write_vectored(&self.io_slices);
9839 self.record_write(status);
9840 debug_assert!(
9841 size <= offered,
9842 "the socket reported {size} bytes written for an offer of {offered}"
9843 );
9844 let stream_size = size.saturating_sub(prefix);
9845 // A write that took everything cut nothing: the walk is
9846 // only for a partial write.
9847 let tail = if size < offered {
9848 h2_transmit::frame_tail(
9849 &self.io_slices[usize::from(prefix > 0)..],
9850 stream_size,
9851 )
9852 } else {
9853 0
9854 };
9855 let kawa = self.core.write_buffer(context, stream_id);
9856 h2_transmit::confirm(kawa, &mut self.io_slices, stream_size);
9857 self.core.handle_write(
9858 context,
9859 stream_id,
9860 H2Written {
9861 size,
9862 from_output: size - stream_size,
9863 tail,
9864 },
9865 status,
9866 &mut pass,
9867 );
9868 }
9869 }
9870 }
9871 }
9872}
9873
9874#[cfg(test)]
9875pub(super) mod tests {
9876 use std::{cell::RefCell, collections::HashMap, rc::Rc};
9877
9878 use super::*;
9879 use crate::{
9880 pool::Pool,
9881 protocol::{
9882 kawa_h1::editor::HttpContext,
9883 mux::{
9884 BackendChange, BackendDelta, Connection,
9885 buffer_source::PoolBufferSource,
9886 connection::EndpointClient,
9887 router::Router,
9888 test_support::{TestListener, connected_socket, test_context},
9889 },
9890 },
9891 socket::SessionTcpStream,
9892 };
9893
9894 // ── H2FloodDetector / H2FloodViolation ─────────────────────────────
9895 //
9896 // H2FloodDetector's own unit tests (threshold trips, half-decay,
9897 // lifetime ceilings, metric-key uniqueness) moved to
9898 // `h2_flood_detector.rs`'s `#[cfg(test)] mod tests` alongside the type —
9899 // its fields are private to that module now, matching `HpackState`,
9900 // `H2FlowControl` and `H2StreamTable`. The old test comparing
9901 // default-constructed vs. explicitly-constructed detectors did not
9902 // move: it compared `H2FloodDetector::default()` against
9903 // `H2FloodDetector::new(...)`, and this extraction removed `impl Default
9904 // for H2FloodDetector` entirely (LIFECYCLE.md invariant 20's last
9905 // self-sampling-clock exception), so there is no second constructor left
9906 // for it to compare against.
9907
9908 // ── Prioriser / scheduler ───────────────────────────────────────────
9909 //
9910 // `Prioriser`'s own unit tests (defaults, the RFC 9218 urgency clamp,
9911 // the `MAX_PRIORITIES` cap, the standalone-PRIORITY acceptance filter,
9912 // the same-urgency incremental rotation) moved to `h2_scheduler.rs`'s
9913 // `#[cfg(test)] mod tests` alongside the type — its fields are private
9914 // to that module now, matching `HpackState`, `H2FlowControl`,
9915 // `H2StreamTable` and `H2FloodDetector`. The pass-scoped scheduling
9916 // tests that used to re-implement `ready_incremental_by_urgency`'s
9917 // arithmetic inline in the test body moved there too, and now drive
9918 // `ReadyIncrementalCensus`'s production methods instead of a copy of
9919 // them.
9920
9921 // ── H2FlowControl ───────────────────────────────────────────────────
9922 //
9923 // H2FlowControl's own unit tests (initial state, coalescing, saturation,
9924 // negative window, the ordering-determinism regression test) moved to
9925 // `h2_flow_control.rs`'s `#[cfg(test)] mod tests` alongside the type —
9926 // its fields are private to that module now, matching `HpackState`.
9927
9928 // ── H2FloodConfig ───────────────────────────────────────────────────
9929 //
9930 // Moved to `h2_flood_detector.rs`'s `#[cfg(test)] mod tests` alongside
9931 // `H2FloodConfig`, `H2FloodViolation` and `H2FloodDetector`.
9932
9933 // ── distribute_overhead ─────────────────────────────────────────────
9934
9935 #[test]
9936 fn test_distribute_overhead_proportional() {
9937 let mut metrics = SessionMetrics::new(None);
9938 let mut overhead_bin = 1000;
9939 let mut overhead_bout = 500;
9940
9941 // Stream transferred 60% of total bytes (not last stream)
9942 distribute_overhead(
9943 &mut metrics,
9944 &mut overhead_bin,
9945 &mut overhead_bout,
9946 (600, 300), // stream_bytes
9947 (1000, 500), // total_bytes
9948 2, // active_streams
9949 false, // is_last_stream
9950 );
9951
9952 assert_eq!(metrics.bin, 600); // 60% of 1000
9953 assert_eq!(metrics.bout, 300); // 60% of 500
9954 assert_eq!(overhead_bin, 400); // 1000 - 600
9955 assert_eq!(overhead_bout, 200); // 500 - 300
9956 }
9957
9958 #[test]
9959 fn test_distribute_overhead_even_split_when_no_bytes() {
9960 let mut metrics = SessionMetrics::new(None);
9961 let mut overhead_bin = 100;
9962 let mut overhead_bout = 200;
9963
9964 // No bytes transferred -> even distribution (not last stream)
9965 distribute_overhead(
9966 &mut metrics,
9967 &mut overhead_bin,
9968 &mut overhead_bout,
9969 (0, 0), // stream_bytes
9970 (0, 0), // total_bytes
9971 4, // active_streams
9972 false, // is_last_stream
9973 );
9974
9975 assert_eq!(metrics.bin, 25); // 100 / 4
9976 assert_eq!(metrics.bout, 50); // 200 / 4
9977 assert_eq!(overhead_bin, 75);
9978 assert_eq!(overhead_bout, 150);
9979 }
9980
9981 #[test]
9982 fn test_distribute_overhead_clamps_to_remaining() {
9983 let mut metrics = SessionMetrics::new(None);
9984 let mut overhead_bin = 10;
9985 let mut overhead_bout = 10;
9986
9987 // Stream claims 100% of bytes but overhead is small (last stream)
9988 distribute_overhead(
9989 &mut metrics,
9990 &mut overhead_bin,
9991 &mut overhead_bout,
9992 (1000, 1000), // stream_bytes
9993 (1000, 1000), // total_bytes
9994 1, // active_streams
9995 true, // is_last_stream
9996 );
9997
9998 assert_eq!(metrics.bin, 10);
9999 assert_eq!(metrics.bout, 10);
10000 assert_eq!(overhead_bin, 0);
10001 assert_eq!(overhead_bout, 0);
10002 }
10003
10004 #[test]
10005 fn test_distribute_overhead_zero_active_streams() {
10006 let mut metrics = SessionMetrics::new(None);
10007 let mut overhead_bin = 100;
10008 let mut overhead_bout = 100;
10009
10010 // 0 active streams (edge case) — last stream gets all remainder
10011 distribute_overhead(
10012 &mut metrics,
10013 &mut overhead_bin,
10014 &mut overhead_bout,
10015 (0, 0),
10016 (0, 0),
10017 0,
10018 true,
10019 );
10020
10021 assert_eq!(metrics.bin, 100); // last stream gets all remaining
10022 assert_eq!(metrics.bout, 100);
10023 assert_eq!(overhead_bin, 0);
10024 assert_eq!(overhead_bout, 0);
10025 }
10026
10027 #[test]
10028 fn test_distribute_overhead_last_stream_gets_remainder() {
10029 let mut metrics1 = SessionMetrics::new(None);
10030 let mut metrics2 = SessionMetrics::new(None);
10031 let mut overhead_bin = 120;
10032 let mut overhead_bout = 120;
10033
10034 // First stream (not last): gets proportional share
10035 distribute_overhead(
10036 &mut metrics1,
10037 &mut overhead_bin,
10038 &mut overhead_bout,
10039 (100, 100), // stream_bytes
10040 (300, 300), // total_bytes
10041 3, // active_streams
10042 false, // is_last_stream
10043 );
10044
10045 let remaining_bin = overhead_bin;
10046 let remaining_bout = overhead_bout;
10047
10048 // Last stream: gets ALL remaining overhead (no rounding loss)
10049 distribute_overhead(
10050 &mut metrics2,
10051 &mut overhead_bin,
10052 &mut overhead_bout,
10053 (100, 100), // stream_bytes
10054 (300, 300), // total_bytes
10055 3, // active_streams
10056 true, // is_last_stream
10057 );
10058
10059 assert_eq!(metrics2.bin, remaining_bin);
10060 assert_eq!(metrics2.bout, remaining_bout);
10061 assert_eq!(overhead_bin, 0, "no remainder bytes should be lost");
10062 assert_eq!(overhead_bout, 0, "no remainder bytes should be lost");
10063 }
10064
10065 // ── H2FlowControl (additional edge cases) ─────────────────────────
10066
10067 #[test]
10068 fn test_flow_control_queue_window_update_cap() {
10069 // Verify DEFAULT_MAX_PENDING_WINDOW_UPDATES reflects 1 + 4*MAX_CONCURRENT_STREAMS
10070 assert_eq!(DEFAULT_MAX_PENDING_WINDOW_UPDATES, 1 + 100 * 4);
10071
10072 // Simulate queue reaching capacity
10073 let cap = DEFAULT_MAX_PENDING_WINDOW_UPDATES;
10074 let mut updates: HashMap<u32, u32> = HashMap::new();
10075 for i in 0..cap as u32 {
10076 updates.insert(i, 1000);
10077 }
10078 assert_eq!(updates.len(), cap);
10079
10080 // A new stream ID beyond capacity should be rejected
10081 let next_stream = cap as u32;
10082 let at_cap = updates.len() >= cap;
10083 assert!(at_cap);
10084 assert!(!updates.contains_key(&next_stream));
10085
10086 // Verify custom max_concurrent_streams produces proportional cap
10087 let custom_cap = 1 + 500_usize * 4;
10088 assert_eq!(custom_cap, 2001);
10089 }
10090
10091 #[test]
10092 fn test_h2_connection_config_defaults() {
10093 let config = H2ConnectionConfig::default();
10094 assert_eq!(config.initial_connection_window, ENLARGED_CONNECTION_WINDOW);
10095 assert_eq!(
10096 config.max_concurrent_streams,
10097 DEFAULT_MAX_CONCURRENT_STREAMS
10098 );
10099 assert_eq!(config.stream_shrink_ratio, 2);
10100 }
10101
10102 #[test]
10103 fn test_h2_connection_config_clamp_window_lower_bound() {
10104 // Below minimum: clamped to DEFAULT_INITIAL_WINDOW_SIZE (65535)
10105 let config = H2ConnectionConfig::new(100, 100, 2);
10106 assert_eq!(
10107 config.initial_connection_window,
10108 DEFAULT_INITIAL_WINDOW_SIZE
10109 );
10110 }
10111
10112 #[test]
10113 fn test_h2_connection_config_clamp_window_upper_bound() {
10114 // Above maximum: clamped to FLOW_CONTROL_MAX_WINDOW (2^31-1)
10115 let config = H2ConnectionConfig::new(u32::MAX, 100, 2);
10116 assert_eq!(config.initial_connection_window, FLOW_CONTROL_MAX_WINDOW);
10117 }
10118
10119 #[test]
10120 fn test_h2_connection_config_clamp_window_exact_minimum() {
10121 // Exactly minimum: no clamping, no zero-increment WINDOW_UPDATE risk
10122 let config = H2ConnectionConfig::new(DEFAULT_INITIAL_WINDOW_SIZE, 100, 2);
10123 assert_eq!(
10124 config.initial_connection_window,
10125 DEFAULT_INITIAL_WINDOW_SIZE
10126 );
10127 // Increment to send would be 0 — the code guards this with `if increment > 0`
10128 let increment = config
10129 .initial_connection_window
10130 .saturating_sub(DEFAULT_INITIAL_WINDOW_SIZE);
10131 assert_eq!(increment, 0);
10132 }
10133
10134 #[test]
10135 fn test_h2_connection_config_clamp_shrink_ratio() {
10136 // Below minimum: clamped to 2 (1 would defeat recycling)
10137 let config = H2ConnectionConfig::new(ENLARGED_CONNECTION_WINDOW, 100, 0);
10138 assert_eq!(config.stream_shrink_ratio, 2);
10139 let config = H2ConnectionConfig::new(ENLARGED_CONNECTION_WINDOW, 100, 1);
10140 assert_eq!(config.stream_shrink_ratio, 2);
10141 }
10142
10143 #[test]
10144 fn test_h2_connection_config_clamp_concurrent_streams() {
10145 let config = H2ConnectionConfig::new(ENLARGED_CONNECTION_WINDOW, 0, 2);
10146 assert_eq!(config.max_concurrent_streams, 1);
10147 }
10148
10149 #[test]
10150 fn test_h2_connection_config_from_optional_uses_defaults() {
10151 let config = H2ConnectionConfig::from_optional(None, None, None);
10152 let defaults = H2ConnectionConfig::default();
10153 assert_eq!(config, defaults);
10154 }
10155
10156 #[test]
10157 fn test_h2_connection_config_from_optional_overrides() {
10158 let config = H2ConnectionConfig::from_optional(Some(2_000_000), Some(500), Some(4));
10159 assert_eq!(config.initial_connection_window, 2_000_000);
10160 assert_eq!(config.max_concurrent_streams, 500);
10161 assert_eq!(config.stream_shrink_ratio, 4);
10162 }
10163
10164 // The old settings-change-negative-window test was removed here, not
10165 // moved: its docstring premise was wrong. `update_initial_window_size`
10166 // (below) only ever adjusts *per-stream* windows on a
10167 // SETTINGS_INITIAL_WINDOW_SIZE change, never the connection-level
10168 // `flow_control.window` that test constructed and mutated directly — so
10169 // it never exercised a real code path. `h2_flow_control.rs`'s test
10170 // `consume_send_window_saturates_at_i32_min_without_wrapping` covers a
10171 // genuinely different, previously-untested boundary of the same
10172 // `consume_send_window` API (the `saturating_sub` floor at `i32::MIN`);
10173 // it does not restore the deleted test's coverage, because the deleted
10174 // test covered nothing real.
10175
10176 #[test]
10177 fn test_flow_control_coalesce_saturates_at_max_increment() {
10178 let max_increment = i32::MAX as u32;
10179 let mut updates: HashMap<u32, u32> = HashMap::new();
10180
10181 // Insert at max and try to coalesce more
10182 updates.insert(1, max_increment);
10183 if let Some(existing) = updates.get_mut(&1) {
10184 *existing = existing.saturating_add(1000).min(max_increment);
10185 }
10186 assert_eq!(*updates.get(&1).unwrap(), max_increment);
10187 }
10188
10189 // ── H2FloodConfig (additional) ───────────────────────────────────
10190 //
10191 // Moved to `h2_flood_detector.rs`'s `#[cfg(test)] mod tests`
10192 // (`test_flood_config_default_matches_constants`, `test_flood_config_equality`).
10193
10194 // ── distribute_overhead (additional edge cases) ───────────────────
10195
10196 #[test]
10197 fn test_distribute_overhead_asymmetric_in_out() {
10198 let mut metrics = SessionMetrics::new(None);
10199 let mut overhead_bin = 1000;
10200 let mut overhead_bout = 1000;
10201
10202 // Stream transferred 100% inbound, 0% outbound (not last stream)
10203 distribute_overhead(
10204 &mut metrics,
10205 &mut overhead_bin,
10206 &mut overhead_bout,
10207 (500, 0), // stream_bytes
10208 (500, 100), // total_bytes
10209 2, // active_streams
10210 false, // is_last_stream
10211 );
10212
10213 assert_eq!(metrics.bin, 1000); // 100% of inbound overhead
10214 assert_eq!(metrics.bout, 0); // 0% of outbound overhead
10215 assert_eq!(overhead_bin, 0);
10216 assert_eq!(overhead_bout, 1000);
10217 }
10218
10219 #[test]
10220 fn test_distribute_overhead_many_streams_accumulate() {
10221 let mut metrics = SessionMetrics::new(None);
10222 let mut overhead_bin = 120;
10223 let mut overhead_bout = 120;
10224
10225 // Three equal streams, each calling distribute_overhead.
10226 // With is_last_stream on the third call, the last stream gets all
10227 // remaining overhead, so no rounding loss occurs.
10228 // call 1: 120 * 100/300 = 40 -> remaining 80
10229 // call 2: 80 * 100/300 = 26 -> remaining 54
10230 // call 3: last stream gets all remaining = 54
10231 // Total distributed: 40 + 26 + 54 = 120 (no loss)
10232 for i in 0..3 {
10233 distribute_overhead(
10234 &mut metrics,
10235 &mut overhead_bin,
10236 &mut overhead_bout,
10237 (100, 100), // stream_bytes
10238 (300, 300), // total_bytes
10239 3, // active_streams
10240 i == 2, // is_last_stream on final call
10241 );
10242 }
10243
10244 assert_eq!(metrics.bin, 120);
10245 assert_eq!(metrics.bout, 120);
10246 // No rounding residual — last stream absorbed the remainder
10247 assert_eq!(overhead_bin, 0);
10248 assert_eq!(overhead_bout, 0);
10249 }
10250
10251 // ── Hex chunk formatting ────────────────────────────────────────────
10252
10253 /// Verify that the Vec<u8> + write!() hex formatting used in
10254 /// handle_data_frame produces output identical to format!("{:x}").
10255 #[test]
10256 fn test_hex_chunk_length_formatting() {
10257 use std::io::Write as _;
10258
10259 let cases: &[(usize, &[u8])] = &[
10260 (1, b"1"),
10261 (15, b"f"),
10262 (16, b"10"),
10263 (255, b"ff"),
10264 (256, b"100"),
10265 (4096, b"1000"),
10266 (65535, b"ffff"),
10267 (65536, b"10000"),
10268 ];
10269
10270 for &(payload_len, expected) in cases {
10271 let mut buf = Vec::with_capacity(16);
10272 let _ = write!(buf, "{payload_len:x}");
10273 assert_eq!(
10274 buf, expected,
10275 "hex formatting mismatch for payload_len={payload_len}"
10276 );
10277 }
10278
10279 // usize::MAX tested separately to avoid temporary lifetime issue
10280 let max_expected = format!("{:x}", usize::MAX);
10281 let mut buf = Vec::with_capacity(16);
10282 let _ = write!(buf, "{:x}", usize::MAX);
10283 assert_eq!(buf, max_expected.as_bytes());
10284 }
10285
10286 // ── Stream-ID allocation / exhaustion ──────────────────────────────────
10287
10288 /// A fresh client connection starts with `last_stream_id == 0`. The first
10289 /// call MUST issue stream `1` (odd, RFC 9113 §5.1.1) and advance the
10290 /// watermark to `2`.
10291 #[test]
10292 fn test_next_stream_id_client_first_allocation() {
10293 let (issued, next) = next_stream_id(0, true).expect("fresh client must allocate");
10294 assert_eq!(issued, 1);
10295 assert_eq!(next, 2);
10296 }
10297
10298 /// Client allocation yields strictly increasing odd identifiers
10299 /// (1, 3, 5, ...) as required by RFC 9113 §5.1.1.
10300 #[test]
10301 fn test_next_stream_id_client_sequence_is_odd_and_monotonic() {
10302 let mut last = 0u32;
10303 let mut issued_ids = Vec::with_capacity(8);
10304 for _ in 0..8 {
10305 let (id, next) = next_stream_id(last, true).expect("unexhausted");
10306 assert_eq!(id & 1, 1, "client stream ids must be odd (RFC 9113 §5.1.1)");
10307 assert!(issued_ids.last().is_none_or(|prev: &u32| id > *prev));
10308 issued_ids.push(id);
10309 last = next;
10310 }
10311 assert_eq!(issued_ids, vec![1, 3, 5, 7, 9, 11, 13, 15]);
10312 }
10313
10314 /// Server-side allocation yields even identifiers. The helper
10315 /// convention is `watermark - 2` for server, `watermark - 1` for client,
10316 /// so both sides share the same monotonically-increasing even watermark.
10317 /// Sōzu never server-pushes, but the helper must be symmetric so push
10318 /// could be enabled without a regression.
10319 #[test]
10320 fn test_next_stream_id_server_is_even() {
10321 // `last = 2` means the most recent allocation advanced the watermark
10322 // to 2; server then issues `2 - 2 = 0`. This is an artefact of the
10323 // shared watermark and only matters in tests — server never uses it.
10324 let (issued, next) = next_stream_id(2, false).expect("server allocation");
10325 assert_eq!(issued & 1, 0, "server stream ids must be even");
10326 assert_eq!(next, 4);
10327 assert_eq!(issued, 2);
10328
10329 let (issued, next) = next_stream_id(next, false).expect("second slot");
10330 assert_eq!(issued, 4);
10331 assert_eq!(issued & 1, 0);
10332 assert_eq!(next, 6);
10333 }
10334
10335 /// The last client-issuable odd stream ID is `STREAM_ID_MAX = 0x7FFF_FFFF`.
10336 /// To issue it the watermark must advance to `STREAM_ID_MAX + 1 = 2³¹`;
10337 /// the caller therefore supplies `last = STREAM_ID_MAX - 1 = 0x7FFF_FFFE`.
10338 /// That call MUST succeed and return the max ID; the post-call watermark
10339 /// sits at `2³¹`, which is the sentinel that makes the next call fail.
10340 #[test]
10341 fn test_next_stream_id_client_final_slot_allocates() {
10342 let last = STREAM_ID_MAX - 1;
10343 let (issued, next) = next_stream_id(last, true).expect("final slot still allocates");
10344 assert_eq!(issued, STREAM_ID_MAX);
10345 assert_eq!(next, STREAM_ID_MAX + 1);
10346 // And the very next call MUST refuse rather than wrap.
10347 assert!(next_stream_id(next, true).is_none());
10348 }
10349
10350 /// Exhaustion case: once the client has issued stream ID `STREAM_ID_MAX`,
10351 /// the watermark sits at `STREAM_ID_MAX + 1`. The next request MUST return
10352 /// `None` — without this guard the helper would issue `STREAM_ID_MAX + 2`
10353 /// (wrapped down to an even id), which would (a) use the reserved
10354 /// high bit and (b) violate the odd-parity invariant for client streams.
10355 #[test]
10356 fn test_next_stream_id_client_exhausted_returns_none() {
10357 let last = STREAM_ID_MAX + 1;
10358 assert!(next_stream_id(last, true).is_none());
10359 }
10360
10361 /// Exhaustion via `checked_add` saturation: defence in depth in case a
10362 /// caller jumps `last_stream_id` close to `u32::MAX`. The helper must
10363 /// not panic nor overflow — it must return `None`.
10364 #[test]
10365 fn test_next_stream_id_saturates_near_u32_max() {
10366 assert!(next_stream_id(u32::MAX, true).is_none());
10367 assert!(next_stream_id(u32::MAX - 1, true).is_none());
10368 }
10369
10370 /// Server-side exhaustion: same guard, even-parity identifier space.
10371 #[test]
10372 fn test_next_stream_id_server_exhausted_returns_none() {
10373 let last = STREAM_ID_MAX + 1;
10374 assert!(next_stream_id(last, false).is_none());
10375 }
10376
10377 /// Regression guard: the helper must never issue a stream ID that
10378 /// exceeds `STREAM_ID_MAX` for either side, no matter where the
10379 /// watermark sits. This walks every value in a neighbourhood of the
10380 /// boundary to rule out off-by-one errors.
10381 #[test]
10382 fn test_next_stream_id_never_exceeds_stream_id_max() {
10383 for last in (STREAM_ID_MAX - 4)..=(STREAM_ID_MAX + 4) {
10384 for is_client in [true, false] {
10385 if let Some((issued, next)) = next_stream_id(last, is_client) {
10386 assert!(
10387 issued <= STREAM_ID_MAX,
10388 "issued id {issued} exceeds STREAM_ID_MAX (last={last}, is_client={is_client})"
10389 );
10390 // `next` is the post-allocation watermark and may sit at
10391 // STREAM_ID_MAX + 1 — the very next call must then return None.
10392 if next > STREAM_ID_MAX {
10393 assert!(
10394 next_stream_id(next, is_client).is_none(),
10395 "second call after final slot must report exhaustion"
10396 );
10397 }
10398 }
10399 }
10400 }
10401 }
10402
10403 /// The helper's `is_client` flag must cleanly split the ID space so that
10404 /// a client and a server peered on the same connection cannot collide.
10405 /// Given the same `last_stream_id`, the two parities must differ by 1.
10406 #[test]
10407 fn test_next_stream_id_client_server_parities_disjoint() {
10408 for last in [0u32, 2, 4, 10, 100, 1_000_000, STREAM_ID_MAX - 3] {
10409 let (client_id, _) = next_stream_id(last, true).unwrap();
10410 let (server_id, _) = next_stream_id(last, false).unwrap();
10411 assert_eq!(client_id & 1, 1);
10412 assert_eq!(server_id & 1, 0);
10413 assert_eq!(client_id.abs_diff(server_id), 1);
10414 }
10415 }
10416
10417 // ── LIFECYCLE §9 invariant 16: any_stream_id_matches ─────────────────
10418 //
10419 // Covers the iteration dispatch used by `any_stream_has_pending_back`.
10420 // Testing the probe directly against a synthetic closure keeps the
10421 // tests independent of the full `Stream` fixture (which requires a
10422 // `Pool` and a fully-built `HttpContext`).
10423
10424 #[test]
10425 fn test_any_stream_id_matches_empty_map_is_false() {
10426 let streams: BTreeMap<StreamId, GlobalStreamId> = BTreeMap::new();
10427 assert!(!any_stream_id_matches(&streams, |_| true));
10428 }
10429
10430 #[test]
10431 fn test_any_stream_id_matches_all_probe_false_is_false() {
10432 let mut streams: BTreeMap<StreamId, GlobalStreamId> = BTreeMap::new();
10433 streams.insert(1, 0);
10434 streams.insert(3, 1);
10435 streams.insert(5, 2);
10436 assert!(!any_stream_id_matches(&streams, |_| false));
10437 }
10438
10439 #[test]
10440 fn test_any_stream_id_matches_any_probe_true_is_true() {
10441 let mut streams: BTreeMap<StreamId, GlobalStreamId> = BTreeMap::new();
10442 streams.insert(1, 0);
10443 streams.insert(3, 1);
10444 streams.insert(5, 2);
10445 // Probe is true only for GlobalStreamId == 1 (i.e. StreamId 3).
10446 assert!(any_stream_id_matches(&streams, |gid| gid == 1));
10447 }
10448
10449 #[test]
10450 fn test_any_stream_id_matches_single_entry() {
10451 let mut streams: BTreeMap<StreamId, GlobalStreamId> = BTreeMap::new();
10452 streams.insert(42, 7);
10453 assert!(any_stream_id_matches(&streams, |gid| gid == 7));
10454 assert!(!any_stream_id_matches(&streams, |gid| gid == 8));
10455 }
10456
10457 #[test]
10458 fn test_any_stream_id_matches_short_circuits() {
10459 let mut streams: BTreeMap<StreamId, GlobalStreamId> = BTreeMap::new();
10460 streams.insert(1, 0);
10461 streams.insert(3, 1);
10462 streams.insert(5, 2);
10463 streams.insert(7, 3);
10464 let mut calls = 0usize;
10465 let result = any_stream_id_matches(&streams, |_| {
10466 calls += 1;
10467 true
10468 });
10469 assert!(result);
10470 // `Iterator::any` short-circuits on the first `true` — so the probe
10471 // must fire at most once in this construction.
10472 assert_eq!(calls, 1);
10473 }
10474
10475 // ── cumulative-stall budget decision (fc_stall_budget_decision) ──
10476
10477 #[test]
10478 fn test_fc_stall_budget_open_window_always_clears() {
10479 // A genuinely open send window is a real un-stall, regardless of prior
10480 // accumulated progress or this pass's drain.
10481 assert_eq!(
10482 fc_stall_budget_decision(false, 0, None),
10483 FcStallAction::Clear
10484 );
10485 assert_eq!(
10486 fc_stall_budget_decision(false, 1, Some(5)),
10487 FcStallAction::Clear
10488 );
10489 assert_eq!(
10490 fc_stall_budget_decision(false, i32::MAX, Some(FC_STALL_CLEAR_FLOOR)),
10491 FcStallAction::Clear
10492 );
10493 }
10494
10495 #[test]
10496 fn test_fc_stall_budget_blocked_arms_and_accumulates() {
10497 // First blocked pass arms with this pass's drain.
10498 assert_eq!(
10499 fc_stall_budget_decision(true, 1, None),
10500 FcStallAction::Arm { progress: 1 }
10501 );
10502 // A blocked pass with no drain keeps the accumulator unchanged, so the
10503 // deadline keeps aging (a window-0 stall makes consumed == 0).
10504 assert_eq!(
10505 fc_stall_budget_decision(true, 0, Some(42)),
10506 FcStallAction::Arm { progress: 42 }
10507 );
10508 // Negative `consumed` is clamped to 0 (defensive; converter.window only
10509 // shrinks, so consumed is >= 0 in practice).
10510 assert_eq!(
10511 fc_stall_budget_decision(true, -10, Some(7)),
10512 FcStallAction::Arm { progress: 7 }
10513 );
10514 }
10515
10516 #[test]
10517 fn test_fc_stall_budget_floor_clears() {
10518 // Reaching the floor in a single pass (a full DATA frame of real
10519 // delivery) clears the deadline.
10520 assert_eq!(
10521 fc_stall_budget_decision(true, FC_STALL_CLEAR_FLOOR as i32, None),
10522 FcStallAction::Clear
10523 );
10524 // Exactly one byte below the floor still arms.
10525 assert_eq!(
10526 fc_stall_budget_decision(true, (FC_STALL_CLEAR_FLOOR - 1) as i32, None),
10527 FcStallAction::Arm {
10528 progress: FC_STALL_CLEAR_FLOOR - 1
10529 }
10530 );
10531 // Prior progress plus this pass crossing the floor clears.
10532 assert_eq!(
10533 fc_stall_budget_decision(true, 1, Some(FC_STALL_CLEAR_FLOOR - 1)),
10534 FcStallAction::Clear
10535 );
10536 }
10537
10538 #[test]
10539 fn test_fc_stall_budget_wu_drip_ages_until_floor() {
10540 // The WINDOW_UPDATE(+1) closure: a 1-byte-per-pass drip must keep the
10541 // deadline armed (aging) for the whole run up to the floor and only
10542 // clear on the pass that reaches it — so a drip granting < floor bytes
10543 // per idle period is reaped, never kept alive. This is the unit-level
10544 // proof that the budget closes the WINDOW_UPDATE-drip vector.
10545 let mut progress: Option<usize> = None;
10546 for pass in 1..FC_STALL_CLEAR_FLOOR {
10547 match fc_stall_budget_decision(true, 1, progress) {
10548 FcStallAction::Arm { progress: p } => {
10549 assert_eq!(p, pass, "drip accumulator off at pass {pass}");
10550 progress = Some(p);
10551 }
10552 FcStallAction::Clear => panic!("drip cleared the deadline early at pass {pass}"),
10553 }
10554 }
10555 // The pass that reaches the floor finally clears.
10556 assert_eq!(
10557 fc_stall_budget_decision(true, 1, progress),
10558 FcStallAction::Clear
10559 );
10560 }
10561
10562 // ── flow-control-stall reaper union (h2_stream_table::collect_timed_out) ──
10563 //
10564 // The two-guard union (bidirectional-silence + flow-control-stall),
10565 // its dedup, its rst_sent/untracked filtering, and its determinism
10566 // (ascending stream_id order — the regression test for this
10567 // extraction's fix) are now unit-tested directly against
10568 // `H2StreamTable::collect_timed_out` in `h2_stream_table.rs`'s own test
10569 // module, without needing a full `ConnectionH2` fixture.
10570
10571 // ── LIFECYCLE §9 invariant 16: any_stream_has_pending_back ───────────
10572
10573 /// Build a minimal `Stream` for invariant-16 probing. Uses the pool
10574 /// plumbing so `back.blocks` / `back.out` exist; every other field is
10575 /// default-valued because the predicate only reads the back buffer.
10576 fn make_stream_for_invariant_16(pool: &Rc<RefCell<Pool>>, session_ulid: Ulid) -> Stream {
10577 let http_ctx = HttpContext {
10578 keep_alive_backend: true,
10579 keep_alive_frontend: true,
10580 sticky_session_found: None,
10581 method: None,
10582 authority: None,
10583 path: None,
10584 status: None,
10585 reason: None,
10586 user_agent: None,
10587 x_request_id: None,
10588 xff_chain: None,
10589 #[cfg(feature = "opentelemetry")]
10590 otel: None,
10591 closing: false,
10592 session_id: session_ulid,
10593 id: Ulid::generate(),
10594 backend_id: None,
10595 cluster_id: None,
10596 cluster_metrics_incarnation: Default::default(),
10597 affinity_key: None,
10598 protocol: Protocol::HTTPS,
10599 public_address: "127.0.0.1:0".parse().unwrap(),
10600 session_address: None,
10601 sticky_name: String::new(),
10602 sticky_session: None,
10603 backend_address: None,
10604 tls_server_name: None,
10605 tls_cert_names: None,
10606 strict_sni_binding: false,
10607 elide_x_real_ip: false,
10608 send_x_real_ip: false,
10609 forwarded_headers: sozu_command_lib::proto::command::ForwardedHeaders::Both,
10610 max_trailer_fields: 128,
10611 trailer_fields: 0,
10612 tls_version: None,
10613 tls_cipher: None,
10614 tls_alpn: None,
10615 sozu_id_header: String::from("Sozu-Id"),
10616 redirect_location: None,
10617 www_authenticate: None,
10618 original_authority: None,
10619 headers_response: Vec::new(),
10620 retry_after_seconds: None,
10621 frontend_redirect_template: None,
10622 redirect_status: None,
10623 tags: None,
10624 forwarding_hop: None,
10625 access_log_message: None,
10626 backends_unavailable: false,
10627 };
10628 Stream::new(
10629 &mut PoolBufferSource::new(Rc::downgrade(pool)),
10630 http_ctx,
10631 crate::protocol::mux::test_support::test_answers(),
10632 65_535,
10633 )
10634 .expect("pool should have capacity for two buffers")
10635 }
10636
10637 fn make_pool_for_invariant_16() -> Rc<RefCell<Pool>> {
10638 // Two buffer slots per stream (front + back), ten stream slots is
10639 // plenty for the tests below.
10640 Rc::new(RefCell::new(Pool::with_capacity(4, 20, 16_384)))
10641 }
10642
10643 /// What `handle_data_frame` does with the DATA payload of a backend
10644 /// response: a 204 or a 304 cannot carry content and is reset, even to
10645 /// HEAD; a response to HEAD only SHOULD NOT and its payload is
10646 /// discarded (RFC 9110 §6.4.1, §9.3.2, §15.3.5, §15.4.5).
10647 #[test]
10648 fn a_backend_response_content_is_forbidden_for_204_and_304_and_discarded_for_head() {
10649 use crate::protocol::kawa_h1::parser::Method;
10650 let pool = make_pool_for_invariant_16();
10651 let mut stream = make_stream_for_invariant_16(&pool, Ulid::generate());
10652 let cases = [
10653 (
10654 Some(Method::Head),
10655 Some(200),
10656 BackendResponseContent::Discarded,
10657 ),
10658 (
10659 Some(Method::Head),
10660 Some(204),
10661 BackendResponseContent::Forbidden,
10662 ),
10663 (
10664 Some(Method::Get),
10665 Some(204),
10666 BackendResponseContent::Forbidden,
10667 ),
10668 (
10669 Some(Method::Get),
10670 Some(304),
10671 BackendResponseContent::Forbidden,
10672 ),
10673 (
10674 Some(Method::Get),
10675 Some(200),
10676 BackendResponseContent::Forwarded,
10677 ),
10678 (
10679 Some(Method::Post),
10680 Some(205),
10681 BackendResponseContent::Forwarded,
10682 ),
10683 (None, None, BackendResponseContent::Forwarded),
10684 ];
10685 for (method, status, expected) in cases {
10686 stream.context.method = method.clone();
10687 stream.context.status = status;
10688 assert_eq!(
10689 ConnectionH2::backend_response_content(&stream.context),
10690 expected,
10691 "{method:?} {status:?}"
10692 );
10693 }
10694 }
10695
10696 #[test]
10697 fn test_any_stream_has_pending_back_empty_map_is_false() {
10698 let pool = make_pool_for_invariant_16();
10699 let ulid = Ulid::generate();
10700 let streams_map: BTreeMap<StreamId, GlobalStreamId> = BTreeMap::new();
10701 let context_streams = vec![make_stream_for_invariant_16(&pool, ulid)];
10702 assert!(!any_stream_has_pending_back(&streams_map, &context_streams));
10703 }
10704
10705 #[test]
10706 fn test_any_stream_has_pending_back_all_drained_is_false() {
10707 let pool = make_pool_for_invariant_16();
10708 let ulid = Ulid::generate();
10709 let context_streams = vec![
10710 make_stream_for_invariant_16(&pool, ulid),
10711 make_stream_for_invariant_16(&pool, ulid),
10712 ];
10713 let mut streams_map: BTreeMap<StreamId, GlobalStreamId> = BTreeMap::new();
10714 streams_map.insert(1, 0);
10715 streams_map.insert(3, 1);
10716 // Both freshly-built streams have empty back.out and back.blocks
10717 // (Kawa::new starts with empty deques).
10718 assert!(!any_stream_has_pending_back(&streams_map, &context_streams));
10719 }
10720
10721 #[test]
10722 fn test_any_stream_has_pending_back_unknown_gid_is_false() {
10723 // LIFECYCLE invariant 16 defence-in-depth: an unknown
10724 // `GlobalStreamId` during a stream-removal race must not panic;
10725 // `.get()` must short-circuit to `false`.
10726 let pool = make_pool_for_invariant_16();
10727 let ulid = Ulid::generate();
10728 let context_streams = vec![make_stream_for_invariant_16(&pool, ulid)];
10729 let mut streams_map: BTreeMap<StreamId, GlobalStreamId> = BTreeMap::new();
10730 // GlobalStreamId 42 is out of range for the 1-element slice above.
10731 streams_map.insert(7, 42);
10732 assert!(!any_stream_has_pending_back(&streams_map, &context_streams));
10733 }
10734
10735 #[test]
10736 fn test_any_stream_has_pending_back_with_pending_blocks_is_true() {
10737 let pool = make_pool_for_invariant_16();
10738 let ulid = Ulid::generate();
10739 let mut stream = make_stream_for_invariant_16(&pool, ulid);
10740 // Push one dummy block — any Block variant is fine; the predicate
10741 // only checks `blocks.is_empty()`.
10742 stream.back.blocks.push_back(kawa::Block::StatusLine);
10743 let mut streams_map: BTreeMap<StreamId, GlobalStreamId> = BTreeMap::new();
10744 streams_map.insert(1, 0);
10745 assert!(any_stream_has_pending_back(&streams_map, &[stream]));
10746 }
10747
10748 #[test]
10749 fn test_any_stream_has_pending_back_with_pending_out_is_true() {
10750 let pool = make_pool_for_invariant_16();
10751 let ulid = Ulid::generate();
10752 let mut stream = make_stream_for_invariant_16(&pool, ulid);
10753 // Non-empty out buffer with no blocks.
10754 stream
10755 .back
10756 .out
10757 .push_back(kawa::OutBlock::Store(kawa::Store::Static(b"partial frame")));
10758 let mut streams_map: BTreeMap<StreamId, GlobalStreamId> = BTreeMap::new();
10759 streams_map.insert(1, 0);
10760 assert!(any_stream_has_pending_back(&streams_map, &[stream]));
10761 }
10762
10763 // ── Mid-pass ready-incremental census consistency ───────────────────
10764 //
10765 // The three scalar bucket-decrement tests moved to `h2_scheduler.rs`.
10766 // They were named after `ready_incremental_by_urgency`, the local
10767 // `HashMap` this step replaced with `ReadyIncrementalCensus`; no symbol
10768 // of that name exists anywhere in the tree any more.
10769 // They used to re-implement the `saturating_sub` + bucket-scoped
10770 // decrement inline in the test body — a copy of the scheduler's
10771 // arithmetic, which could not catch the scheduler getting it wrong.
10772 // They now call `ReadyIncrementalCensus::note_ineligible` and
10773 // `::incremental_peer_count`, which is the code `write_streams` runs.
10774
10775 // ── h2.streams.ready_incremental.by_urgency aggregate ────────────────
10776
10777 /// Read the process-local `h2.streams.ready_incremental.by_urgency` gauge,
10778 /// treating an absent key as 0. `dump_local_proxy_metrics` is a
10779 /// non-draining filter over the proxy `MetricsMap`, so repeated reads are
10780 /// side-effect free and the key is the raw metric name.
10781 fn ready_incremental_gauge() -> i64 {
10782 use sozu_command::proto::command::filtered_metrics::Inner;
10783 crate::metrics::METRICS.with(|metrics| {
10784 metrics
10785 .borrow_mut()
10786 .dump_local_proxy_metrics()
10787 .get(names::h2::STREAMS_READY_INCREMENTAL_BY_URGENCY)
10788 .and_then(|fm| fm.inner.as_ref())
10789 .and_then(|inner| match inner {
10790 Inner::Gauge(v) => Some(*v as i64),
10791 _ => None,
10792 })
10793 .unwrap_or(0)
10794 })
10795 }
10796
10797 /// A [`BufferSource`] that hands over pooled wire buffers exactly as the
10798 /// real one does but refuses every scratch request.
10799 ///
10800 /// It exists to separate the two halves of the contract. Refusing
10801 /// `checkout` is already reachable by sizing a pool, and the H2
10802 /// simulator's exhaustion scenario does exactly that. Refusing `scratch`
10803 /// is reachable ONLY through a caller-supplied source: the proxy's own
10804 /// [`PoolBufferSource`] hands out a fresh `Vec` and never says no. That
10805 /// asymmetry is the point of the trait being caller-implemented rather
10806 /// than a method on `Pool`, so it needs a test that could not be written
10807 /// without it.
10808 struct ScratchStarvedSource(PoolBufferSource);
10809
10810 impl super::super::buffer_source::BufferSource for ScratchStarvedSource {
10811 fn checkout(&mut self) -> Option<crate::pool::Checkout> {
10812 self.0.checkout()
10813 }
10814
10815 fn scratch(&mut self) -> Option<Vec<u8>> {
10816 None
10817 }
10818 }
10819
10820 fn loopback_socket_handle() -> mio::net::TcpStream {
10821 mio::net::TcpStream::connect(
10822 format!("127.0.0.1:{}", crate::testing::provide_port())
10823 .parse()
10824 .expect("loopback address must parse"),
10825 )
10826 .expect("mio connect must return a socket handle")
10827 }
10828
10829 fn connection_from_source(
10830 buffers: &mut dyn super::super::buffer_source::BufferSource,
10831 ) -> Option<H2Shell<mio::net::TcpStream>> {
10832 H2Shell::new(
10833 Ulid::generate(),
10834 loopback_socket_handle(),
10835 Position::Server,
10836 buffers,
10837 H2FloodConfig::default(),
10838 H2ConnectionConfig::default(),
10839 Duration::from_secs(30),
10840 None,
10841 Duration::from_secs(30),
10842 Some((H2StreamId::Zero, CLIENT_PREFACE_SIZE)),
10843 Ready::READABLE | Ready::HUP | Ready::ERROR,
10844 )
10845 }
10846
10847 /// The HPACK half of the ownership contract is real, not decorative: the
10848 /// codec pair and its scratch buffers are acquired from the connection's
10849 /// buffer source, and a source that refuses one refuses the connection.
10850 ///
10851 /// Both arms run against the SAME pool, with enough buffers for the
10852 /// connection's stream-0 checkout. So the only difference between the two
10853 /// outcomes is the source's answer to `scratch`, which is what stops this
10854 /// from passing for the unrelated reason that the pool ran dry.
10855 #[test]
10856 fn a_source_that_refuses_scratch_refuses_the_connection() {
10857 let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
10858
10859 assert!(
10860 connection_from_source(&mut PoolBufferSource::new(Rc::downgrade(&pool))).is_some(),
10861 "the pool has free buffers, so a source that grants scratch must build a connection",
10862 );
10863 assert!(
10864 connection_from_source(&mut ScratchStarvedSource(PoolBufferSource::new(
10865 Rc::downgrade(&pool)
10866 )))
10867 .is_none(),
10868 "a source that refuses the HPACK scratch must refuse the connection: the codec pair \
10869 is under the same ownership contract as the wire buffers, not allocated behind it",
10870 );
10871 }
10872
10873 /// `h2.streams.ready_incremental.by_urgency` is an aggregate across every
10874 /// live H2 connection, so a connection must hand back exactly what it
10875 /// contributed, on every close path. `H2Shell`'s [`Drop`] is the single
10876 /// decrement site; this pins the round trip.
10877 ///
10878 /// Before this was folded into `last_gauge_snapshot`, the metric was an
10879 /// absolute `gauge!` set from inside `write_streams`: connection B
10880 /// overwrote connection A's value, nothing was ever released, and the
10881 /// dashboard read "whatever the last writer wrote" instead of a sum.
10882 ///
10883 /// `METRICS` is a thread-local, so this asserts the DELTA around one
10884 /// connection's lifetime — robust to any starting value.
10885 ///
10886 /// To SEE THIS RED: delete `self.core.release_connection_gauges();`
10887 /// from `impl Drop for H2Shell`, leaving the
10888 /// `self.record_pending_metrics();` below it. The live-delta assertion
10889 /// still passes — the shell still records what the core queued — and the
10890 /// post-drop one fails with `left: 3, right: 0`: the connection's
10891 /// contribution outliving the connection, which is the
10892 /// aggregate drifting upward by 3 for the rest of the worker's life.
10893 #[test]
10894 fn dropping_a_connection_releases_its_ready_incremental_contribution() {
10895 let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
10896
10897 // A non-blocking connect to a (likely unused) loopback port returns a
10898 // real socket handle immediately, regardless of whether the connection
10899 // completes; nothing below reads or writes it.
10900 let socket = mio::net::TcpStream::connect(
10901 format!("127.0.0.1:{}", crate::testing::provide_port())
10902 .parse()
10903 .expect("loopback address must parse"),
10904 )
10905 .expect("mio connect must return a socket handle");
10906
10907 let before = ready_incremental_gauge();
10908
10909 let mut connection = H2Shell::new(
10910 Ulid::generate(),
10911 socket,
10912 Position::Server,
10913 &mut PoolBufferSource::new(Rc::downgrade(&pool)),
10914 H2FloodConfig::default(),
10915 H2ConnectionConfig::default(),
10916 Duration::from_secs(30),
10917 None,
10918 Duration::from_secs(30),
10919 Some((H2StreamId::Zero, CLIENT_PREFACE_SIZE)),
10920 Ready::READABLE | Ready::HUP | Ready::ERROR,
10921 )
10922 .expect("a pool with free buffers must yield an H2 connection");
10923
10924 // What the tail of `write_streams` writes once the pass's bucket map
10925 // is final, published by the `gauge_connection_state` call that
10926 // follows it — then handed to the aggregator by the shell, which is
10927 // what `H2Shell::settled` does at the end of every real pass.
10928 connection.core.ready_incremental_streams = 3;
10929 connection.core.gauge_connection_state();
10930 connection.record_pending_metrics();
10931
10932 assert_eq!(
10933 ready_incremental_gauge() - before,
10934 3,
10935 "gauge_connection_state must add this connection's ready-incremental \
10936 count to the aggregate as a signed delta"
10937 );
10938
10939 drop(connection);
10940
10941 assert_eq!(
10942 ready_incremental_gauge(),
10943 before,
10944 "Drop must subtract exactly the contribution the connection made, \
10945 returning the aggregate to its prior value"
10946 );
10947 }
10948
10949 /// The core computes the gauge arithmetic and stops there: writing the
10950 /// worker-local `METRICS` aggregator is the shell's step, through
10951 /// [`H2Shell::record_pending_metrics`].
10952 ///
10953 /// This is the property that makes the H2 core drivable by a deterministic
10954 /// simulator, which is the whole point of returning the metric rather than
10955 /// emitting it: a driver with no aggregator sees the same
10956 /// [`MetricEvent`]s that production folds into `METRICS`, and can fold
10957 /// them into a shadow model instead. `protocol/udp/`'s core already works
10958 /// this way and `sim/tests/udp_simulation.rs` already reads it that way.
10959 ///
10960 /// The sibling test above pins the arithmetic; this one pins *who writes*,
10961 /// which no assertion on the aggregate's value can distinguish — a core
10962 /// that emitted directly and a shell that records what the core queued
10963 /// leave the aggregate identical.
10964 ///
10965 /// To SEE THIS RED: in [`ConnectionH2::gauge_connection_state`], put
10966 /// `gauge_add!(names::h2::STREAMS_READY_INCREMENTAL_BY_URGENCY, dr);`
10967 /// back in place of the
10968 /// `self.metric_events.push(MetricEvent::StreamsReadyIncrementalByUrgency(dr));`
10969 /// push. The first assertion then fails with `left: <before + 5>, right:
10970 /// <before>` — the core reaching the aggregator behind the shell's back.
10971 #[test]
10972 fn the_core_queues_gauge_deltas_instead_of_writing_the_metrics_registry() {
10973 let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
10974 let before = ready_incremental_gauge();
10975
10976 let mut connection =
10977 connection_from_source(&mut PoolBufferSource::new(Rc::downgrade(&pool)))
10978 .expect("a pool with free buffers must yield an H2 connection");
10979
10980 connection.core.ready_incremental_streams = 5;
10981 connection.core.gauge_connection_state();
10982
10983 assert_eq!(
10984 ready_incremental_gauge(),
10985 before,
10986 "the core must not write METRICS: `gauge_connection_state` queues \
10987 a MetricEvent and the shell decides when it is recorded"
10988 );
10989
10990 let queued: Vec<MetricEvent> = connection.core.drain_metric_events().collect();
10991 assert!(
10992 queued.contains(&MetricEvent::StreamsReadyIncrementalByUrgency(5)),
10993 "the queued events must carry the signed delta the core computed, \
10994 got {queued:?}"
10995 );
10996 assert_eq!(
10997 ready_incremental_gauge(),
10998 before,
10999 "draining is not recording: taking the events out of the core must \
11000 not move the aggregate either"
11001 );
11002
11003 for event in queued {
11004 record_metric(event);
11005 }
11006
11007 assert_eq!(
11008 ready_incremental_gauge() - before,
11009 5,
11010 "recording the drained events must produce exactly the aggregate \
11011 the old in-core `gauge_add!` produced"
11012 );
11013
11014 drop(connection);
11015
11016 assert_eq!(
11017 ready_incremental_gauge(),
11018 before,
11019 "the released snapshot must travel the same queue and rebalance to \
11020 the prior value"
11021 );
11022 }
11023
11024 /// Read a process-local counter by its raw metric key, treating an absent
11025 /// key as 0. Counter sibling of [`ready_incremental_gauge`], and
11026 /// `dump_local_proxy_metrics` is non-draining here too, so repeated reads
11027 /// are side-effect free.
11028 fn proxy_counter(key: &str) -> i64 {
11029 use sozu_command::proto::command::filtered_metrics::Inner;
11030 crate::metrics::METRICS.with(|metrics| {
11031 metrics
11032 .borrow_mut()
11033 .dump_local_proxy_metrics()
11034 .get(key)
11035 .and_then(|fm| fm.inner.as_ref())
11036 .and_then(|inner| match inner {
11037 Inner::Count(v) => Some(*v),
11038 _ => None,
11039 })
11040 .unwrap_or(0)
11041 })
11042 }
11043
11044 /// A counter the core queues is recorded when the entry point that
11045 /// produced it RETURNS — not whenever the connection is eventually
11046 /// dropped.
11047 ///
11048 /// This is the property the gauges did not need. A gauge delta recorded
11049 /// late still sums to the same value, so slice one could be correct with a
11050 /// drain only in `settled` and `Drop`. A counter recorded late is a
11051 /// counter attributed to the wrong minute: `LocalDrain` is cumulative and
11052 /// scraped periodically, so deferring one to connection teardown moves it
11053 /// past however many scrapes the connection outlives. `Drop` remains the
11054 /// backstop that stops it being *lost*; this pins that the backstop is not
11055 /// where it normally happens.
11056 ///
11057 /// `graceful_goaway` is the cheapest real producer to drive: one public
11058 /// call on a fresh connection reaches `ConnectionH2::send_initial_goaway`,
11059 /// which queues both `MetricEvent::GoAwaySent` and
11060 /// `MetricEvent::GoAwayFrameSent`.
11061 ///
11062 /// To SEE THIS RED: delete `self.record_pending_metrics();` from
11063 /// `H2Shell::settled`. Both assertions before `drop` fail with
11064 /// `left: 0, right: 1` — the counters sitting in the core's queue,
11065 /// arriving only when the connection is torn down.
11066 #[test]
11067 fn a_queued_counter_is_recorded_when_its_entry_point_returns() {
11068 let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
11069 let sent_before = proxy_counter("h2.goaway.sent.no_error");
11070 let frames_before = proxy_counter(names::h2::FRAMES_TX_GOAWAY);
11071
11072 let mut connection =
11073 connection_from_source(&mut PoolBufferSource::new(Rc::downgrade(&pool)))
11074 .expect("a pool with free buffers must yield an H2 connection");
11075
11076 connection.graceful_goaway(Instant::now());
11077
11078 assert_eq!(
11079 proxy_counter("h2.goaway.sent.no_error") - sent_before,
11080 1,
11081 "the GOAWAY decision must reach METRICS when `graceful_goaway` \
11082 returns, not at teardown"
11083 );
11084 assert_eq!(
11085 proxy_counter(names::h2::FRAMES_TX_GOAWAY) - frames_before,
11086 1,
11087 "so must the frame the serializer actually wrote"
11088 );
11089 }
11090
11091 /// One `MetricEvent::RstStreamSent` records BOTH counters its two macro
11092 /// lines used to record: the frame-type total and the per-error-code
11093 /// breakdown.
11094 ///
11095 /// Folding two macro lines into one event is only safe where both were
11096 /// unconditional and adjacent, which is why the GOAWAY pair was NOT folded
11097 /// — `h2.frames.tx.goaway` sits inside `serializer::gen_goaway`'s `Ok`
11098 /// arm while `h2.goaway.sent.*` fires before it, so one event would record
11099 /// a frame on the wire that a serialisation failure never wrote. The RST
11100 /// pair has no such gap, and this pins that folding it kept both counters.
11101 ///
11102 /// To SEE THIS RED: delete either line from the
11103 /// `MetricEvent::RstStreamSent` arm of `record_metric`. The surviving
11104 /// assertion passes and the other fails with `left: 0, right: 1` — half a
11105 /// merge, which is exactly what a merged pair can silently become.
11106 #[test]
11107 fn one_rst_stream_sent_event_records_both_of_its_counters() {
11108 let frames_before = proxy_counter(names::h2::FRAMES_TX_RST_STREAM);
11109 let by_code_before = proxy_counter(metric_for_rst_stream_sent(H2Error::Cancel));
11110
11111 record_metric(MetricEvent::RstStreamSent(H2Error::Cancel));
11112
11113 assert_eq!(
11114 proxy_counter(names::h2::FRAMES_TX_RST_STREAM) - frames_before,
11115 1,
11116 "the frame-type total must still tick"
11117 );
11118 assert_eq!(
11119 proxy_counter(metric_for_rst_stream_sent(H2Error::Cancel)) - by_code_before,
11120 1,
11121 "and so must the per-error-code breakdown"
11122 );
11123 }
11124
11125 // ── Clock hygiene: every deadline reads `ConnectionH2::now` ──────────
11126 //
11127 // `Mux` is the only clock sampler; the H2 core reads the snapshot it
11128 // mirrors into `ConnectionH2::now`. A reintroduced `Instant::now()` in
11129 // the core would still "work" in production — it would just be reading a
11130 // second clock — so these two tests advance ONLY the connection's
11131 // snapshot and prove the decision moves with it. Neither sleeps.
11132
11133 // ── TLS backpressure: the GoAway truncation vector (#1454) ──────────
11134 //
11135 // Until this harness existed, NO test anywhere instantiated a
11136 // `ConnectionH2` over a handler whose `socket_wants_write()` could return
11137 // `true`. `mio::net::TcpStream` and `SessionTcpStream` both take the
11138 // trait's `false` default and only `FrontRustls` overrides it, so every
11139 // branch that asks "does rustls still hold records?" was statically dead
11140 // in the test suite — including the one whose own comment calls it the
11141 // primary truncation vector. These are the first tests that branch class
11142 // has ever had against a handler that answers `true`.
11143
11144 /// A `SocketHandler` that models rustls-over-a-blocked-kernel: it holds
11145 /// `pending` records, and each empty-buffer flush drains `drain_per_flush`
11146 /// of them. `drain_per_flush = 0` is a kernel that accepts nothing, which
11147 /// is the backpressure case; a positive value is a kernel that takes them.
11148 ///
11149 /// Modelling the state rather than scripting an answer sequence is
11150 /// deliberate: the close path queries `socket_wants_write()` a number of
11151 /// times that depends on which branches it takes, so a positional script
11152 /// would pin the query COUNT instead of the behaviour and would have to be
11153 /// rewritten by anyone who added a query.
11154 struct BackpressuredTlsSocket {
11155 stream: mio::net::TcpStream,
11156 pending: std::cell::Cell<usize>,
11157 drain_per_flush: usize,
11158 flushes: std::cell::Cell<usize>,
11159 /// An empty-buffer flush that leaves records behind answers
11160 /// `WouldBlock`, as `FrontRustls` does when the kernel refused them,
11161 /// instead of `Continue`. Off by default, so every test written
11162 /// before this field is byte-for-byte unaffected.
11163 refuse_flush: bool,
11164 /// Scripted `(size, status)` answers for `socket_write_vectored`,
11165 /// consumed front to back. An EMPTY script — the default, and what
11166 /// every test written before this field had — delegates to the real
11167 /// loopback socket, so those tests are byte-for-byte unaffected.
11168 ///
11169 /// The record model above cannot express the pair this scripts.
11170 /// `pending`/`drain_per_flush` answer "does rustls still hold
11171 /// records?"; they say nothing about what a *vectored* write
11172 /// reported, because that method delegated to the kernel and the
11173 /// kernel only ever answers `(0, WouldBlock)` when it blocks.
11174 /// `FrontRustls` does not: `socket_write_vectored` accumulates
11175 /// `buffered_size` from `session.writer().write(..)` — plaintext
11176 /// rustls took off the caller's hands — and separately sets
11177 /// `can_write = false` when `write_tls` hits `WouldBlock` against the
11178 /// kernel (`socket.rs`). The two are independent, so
11179 /// `(size > 0, WouldBlock)` is a shape ONLY the TLS handler returns.
11180 /// That is why the script lives on this fixture rather than on a
11181 /// second one: it is the same handler answering the same question the
11182 /// record model already answers, one layer down.
11183 ///
11184 /// Each `size` is a CAP, clamped to what the gather actually offered:
11185 /// a scripted count above the offer would trip the write shell's own
11186 /// `debug_assert!(size <= offered)`, and reddening through a
11187 /// production assertion is the tree noticing rather than the test.
11188 vectored_script: std::collections::VecDeque<(usize, SocketResult)>,
11189 /// Scripted `(size, status)` answers for a NON-EMPTY `socket_write`,
11190 /// consumed front to back exactly like `vectored_script`. An empty
11191 /// script — the default — delegates to the real loopback socket, so
11192 /// every test written before this field is byte-for-byte unaffected.
11193 ///
11194 /// The record model cannot express this either. `pending` /
11195 /// `drain_per_flush` answer "does rustls still hold records?", which
11196 /// is the EMPTY-buffer flush question; this one is "did the kernel
11197 /// take the bytes `flush_output_to_socket` handed it?". A loopback
11198 /// socket with default buffers always takes 13 bytes of
11199 /// WINDOW_UPDATE, so the stalled branch of the control-frame drains
11200 /// is unreachable without scripting the answer.
11201 ///
11202 /// Each `size` is a CAP, clamped to what the caller actually offered:
11203 /// `flush_output_to_socket` feeds the answer straight to
11204 /// `H2Output::consume`, which asserts it never passes the queue.
11205 write_script: std::collections::VecDeque<(usize, SocketResult)>,
11206 /// How many times `socket_write_vectored` was called, whether the
11207 /// answer came from the script or from the delegated loopback socket.
11208 ///
11209 /// Counting BOTH is what makes this a falsifiable witness. Counting
11210 /// only the scripted answers bounds the total by the script's length,
11211 /// so an assertion that the loop did NOT go round again could only
11212 /// ever deviate downwards and could never fail — and the extra
11213 /// delegated rounds are exactly what such an assertion has to be able
11214 /// to see.
11215 vectored_calls: usize,
11216 /// How many NON-EMPTY `socket_write` calls reached this handler. On
11217 /// `FrontRustls` each one ends in its own `write_tls`, so each one is
11218 /// one `writev(2)` on the wire; an empty-buffer flush is counted by
11219 /// `flushes` instead.
11220 writes: usize,
11221 }
11222
11223 impl BackpressuredTlsSocket {
11224 fn new(stream: mio::net::TcpStream, pending: usize, drain_per_flush: usize) -> Self {
11225 Self {
11226 stream,
11227 pending: std::cell::Cell::new(pending),
11228 drain_per_flush,
11229 flushes: std::cell::Cell::new(0),
11230 refuse_flush: false,
11231 vectored_script: std::collections::VecDeque::new(),
11232 write_script: std::collections::VecDeque::new(),
11233 vectored_calls: 0,
11234 writes: 0,
11235 }
11236 }
11237 }
11238
11239 impl SocketHandler for BackpressuredTlsSocket {
11240 fn socket_read(&mut self, buf: &mut [u8]) -> (usize, SocketResult) {
11241 self.stream.socket_read(buf)
11242 }
11243
11244 fn socket_write(&mut self, buf: &[u8]) -> (usize, SocketResult) {
11245 if buf.is_empty() {
11246 self.flushes.set(self.flushes.get() + 1);
11247 let drained = self.drain_per_flush.min(self.pending.get());
11248 self.pending.set(self.pending.get() - drained);
11249 if self.refuse_flush && self.pending.get() > 0 {
11250 return (0, SocketResult::WouldBlock);
11251 }
11252 return (0, SocketResult::Continue);
11253 }
11254 self.writes += 1;
11255 match self.write_script.pop_front() {
11256 Some((cap, status)) => (cap.min(buf.len()), status),
11257 None => self.stream.socket_write(buf),
11258 }
11259 }
11260
11261 fn socket_write_vectored(&mut self, bufs: &[IoSlice]) -> (usize, SocketResult) {
11262 self.vectored_calls += 1;
11263 match self.vectored_script.pop_front() {
11264 Some((cap, status)) => {
11265 let offered: usize = bufs.iter().map(|slice| slice.len()).sum();
11266 (cap.min(offered), status)
11267 }
11268 None => self.stream.socket_write_vectored(bufs),
11269 }
11270 }
11271
11272 /// The override that makes this harness worth having.
11273 fn socket_wants_write(&self) -> bool {
11274 self.pending.get() > 0
11275 }
11276
11277 fn socket_ref(&self) -> &mio::net::TcpStream {
11278 &self.stream
11279 }
11280
11281 fn socket_mut(&mut self) -> &mut mio::net::TcpStream {
11282 &mut self.stream
11283 }
11284
11285 fn peer_addr(&self) -> Option<std::net::SocketAddr> {
11286 mio::net::TcpStream::peer_addr(&self.stream).ok()
11287 }
11288
11289 fn protocol(&self) -> crate::socket::TransportProtocol {
11290 crate::socket::TransportProtocol::Tls1_3
11291 }
11292
11293 fn read_error(&self) {}
11294
11295 fn write_error(&self) {}
11296 }
11297
11298 /// `state` is a parameter rather than a second copy of this fixture: the
11299 /// close decision reads it as `H2State::GoAway` and the write-pass
11300 /// finalization reads it as `H2State::Header`, and both need the same
11301 /// handler underneath.
11302 fn connection_with_backpressure(
11303 pool: &Rc<RefCell<Pool>>,
11304 pending: usize,
11305 drain_per_flush: usize,
11306 state: H2State,
11307 ) -> (H2Shell<BackpressuredTlsSocket>, std::net::TcpStream) {
11308 let (socket, peer) = connected_socket();
11309 let mut connection = H2Shell::new(
11310 Ulid::generate(),
11311 BackpressuredTlsSocket::new(socket, pending, drain_per_flush),
11312 Position::Server,
11313 &mut PoolBufferSource::new(Rc::downgrade(pool)),
11314 H2FloodConfig::default(),
11315 H2ConnectionConfig::default(),
11316 Duration::from_secs(30),
11317 None,
11318 Duration::from_secs(30),
11319 Some((H2StreamId::Zero, CLIENT_PREFACE_SIZE)),
11320 Ready::WRITABLE | Ready::HUP | Ready::ERROR,
11321 )
11322 .expect("a pool with free buffers must yield an H2 connection");
11323 connection.core.state = state;
11324 (connection, peer)
11325 }
11326
11327 /// Records that survive the flush hold the connection open.
11328 ///
11329 /// This is the branch whose own comment calls it the primary truncation
11330 /// vector: closing here sends FIN and destroys the records rustls is still
11331 /// holding, which the client reads as a truncated response.
11332 ///
11333 /// TO SEE THIS RED: delete the `self.ensure_tls_flushed(tls_wants_write);`
11334 /// call in the `CloseAction::ReArmAndContinue` arm of
11335 /// `ConnectionH2::dispatch_writable_state_after_flush`, which is where
11336 /// `H2Shell::writable`'s `H2State::GoAway` branch settles. The WRITABLE
11337 /// *event* bit is then never re-signalled and this test fails on its OWN
11338 /// assertion, `the WRITABLE event must be re-signalled so the event loop
11339 /// retries the flush`. The recipe deliberately does not swap
11340 /// `TlsFlushPhase::AfterFlush` for `BeforeFlush`: that reddens through the
11341 /// production `unreachable!`, which is someone else's assertion and would
11342 /// pass whatever this test claimed.
11343 #[test]
11344 fn a_flush_that_does_not_drain_keeps_the_connection_open() {
11345 let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
11346 // A kernel that accepts nothing: every flush leaves the records.
11347 let (mut connection, _peer) = connection_with_backpressure(&pool, 2, 0, H2State::GoAway);
11348 let mut context = test_context(&pool);
11349 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
11350
11351 // Premise: the handler really does report buffered records. Without
11352 // this the assertions below would pass against a handler taking the
11353 // trait's `false` default, which is exactly the blind spot #1454 names.
11354 assert!(
11355 connection.socket.socket_wants_write(),
11356 "premise: this harness must report buffered TLS records"
11357 );
11358
11359 let result = connection.writable(&mut context, EndpointClient(&mut router));
11360
11361 assert!(
11362 matches!(result, MuxResult::Continue),
11363 "records still buffered: the session must stay open, got {result:?}"
11364 );
11365 // `Continue` alone does not discriminate: `force_disconnect`'s server
11366 // arm also returns it for a live peer with records pending. The re-arm
11367 // path returns from the GoAway arm BEFORE reaching `force_disconnect`,
11368 // so the state is still `GoAway`; the fall-through would have gone
11369 // through `force_disconnect`, which sets `H2State::Error` first.
11370 assert!(
11371 matches!(connection.core.state, H2State::GoAway),
11372 "the GoAway arm must re-arm and return, not fall through to \
11373 force_disconnect, got {:?}",
11374 connection.core.state
11375 );
11376 // Not `readiness.interest`: that bit is this fixture's own argument to
11377 // `ConnectionH2::new` and nothing on this path clears it, so asserting
11378 // on it would assert on the harness. The edge-triggered re-arm is the
11379 // EVENT bit, set by `ensure_tls_flushed` -> `signal_pending_write`.
11380 assert!(
11381 connection.core.readiness.event.is_writable(),
11382 "the WRITABLE event must be re-signalled so the event loop retries \
11383 the flush, got {:?}",
11384 connection.core.readiness
11385 );
11386 // Not `socket_wants_write()`: with `drain_per_flush = 0` that can never
11387 // change, so it is a tautology of the harness no production edit can
11388 // falsify. The flush COUNT is falsifiable — a GoAway arm that skipped
11389 // its own flush would leave it at 1.
11390 assert!(
11391 connection.socket.flushes.get() >= 2,
11392 "the preamble and the GoAway arm must each attempt a flush, got {}",
11393 connection.socket.flushes.get()
11394 );
11395 }
11396
11397 /// A flush the kernel accepts closes within ONE `writable()` call.
11398 ///
11399 /// This is the tick-count assertion. The pre-image asked its second
11400 /// question inline, immediately after the flush; a split that returned
11401 /// `Continue` and waited to be called again would still deliver the bytes,
11402 /// but would double the latency of every close under backpressure and
11403 /// would strand the connection against a peer that never re-arms.
11404 ///
11405 /// TO SEE THIS RED: in `H2Shell::writable`, add
11406 /// `return MuxResult::Continue;` immediately after the
11407 /// `self.flush_tls_records();` inside the `H2WritableStateTarget::Flush`
11408 /// arm — the flush the `H2State::GoAway` branch asks for, performed and
11409 /// then deferred on. The first call then
11410 /// returns `Continue` and the final assertion fails with `a drained flush
11411 /// must reach the disconnect in the SAME writable call`. Measured: `1115
11412 /// passed; 2 failed`, the sibling being
11413 /// `a_flush_that_does_not_drain_keeps_the_connection_open`, which never
11414 /// reaches its own post-flush answer under the deferral either. Dropping
11415 /// the flush as well would redden the premise at the top of the test
11416 /// instead, with a different message.
11417 #[test]
11418 fn a_flush_that_succeeds_closes_within_one_writable_call() {
11419 let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
11420 // Two records, one drained per flush: the preamble takes one and the
11421 // GoAway arm's own flush takes the other, so the post-flush query is
11422 // the first one that can answer `false`.
11423 let (mut connection, _peer) = connection_with_backpressure(&pool, 2, 1, H2State::GoAway);
11424 let mut context = test_context(&pool);
11425 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
11426
11427 assert!(
11428 connection.socket.socket_wants_write(),
11429 "premise: this harness must report buffered TLS records"
11430 );
11431
11432 let result = connection.writable(&mut context, EndpointClient(&mut router));
11433
11434 assert!(
11435 !connection.socket.socket_wants_write(),
11436 "premise: both flushes landed, so nothing is pending any more"
11437 );
11438 assert!(
11439 connection.socket.flushes.get() >= 2,
11440 "the preamble and the GoAway arm must each attempt a flush, got {}",
11441 connection.socket.flushes.get()
11442 );
11443 assert!(
11444 !matches!(result, MuxResult::Continue),
11445 "a drained flush must reach the disconnect in the SAME writable \
11446 call, not defer it to another tick: got {result:?}"
11447 );
11448 }
11449
11450 /// A GoAway flush the kernel refuses keeps the connection open WITHOUT
11451 /// re-raising WRITABLE: the kernel owes the socket its next edge, and a
11452 /// synthetic one made `Mux::ready_inner` repeat the refused flush on
11453 /// every inner iteration until `MAX_LOOP_ITERATIONS`
11454 /// (sozu-proxy/sozu#1788). Once the kernel drains, the next writable
11455 /// pass flushes the records and reaches the disconnect, so no record is
11456 /// lost on the way to the close.
11457 ///
11458 /// TO SEE THIS RED: drop `&& !self.kernel_refused_write` from
11459 /// `ConnectionH2::ensure_tls_flushed`. The refused pass then re-raises
11460 /// the WRITABLE event.
11461 #[test]
11462 fn a_refused_goaway_flush_waits_for_the_kernel_edge_then_closes() {
11463 let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
11464 let (mut connection, _peer) = connection_with_backpressure(&pool, 2, 0, H2State::GoAway);
11465 connection.socket.refuse_flush = true;
11466 let mut context = test_context(&pool);
11467 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
11468 connection.core.readiness.event.insert(Ready::WRITABLE);
11469
11470 let result = connection.writable(&mut context, EndpointClient(&mut router));
11471
11472 assert!(
11473 matches!(result, MuxResult::Continue),
11474 "records still buffered: the session must stay open, got {result:?}"
11475 );
11476 assert!(
11477 matches!(connection.core.state, H2State::GoAway),
11478 "the GoAway arm must keep the connection, got {:?}",
11479 connection.core.state
11480 );
11481 assert!(
11482 connection.core.readiness.interest.is_writable(),
11483 "WRITABLE interest must stay so the kernel edge is delivered"
11484 );
11485 assert!(
11486 !connection.core.readiness.event.is_writable(),
11487 "a refused flush must leave WRITABLE to the kernel edge, got {:?}",
11488 connection.core.readiness
11489 );
11490
11491 // The peer reads: the kernel takes the records and raises its edge.
11492 connection.socket.drain_per_flush = 2;
11493 connection.core.readiness.event.insert(Ready::WRITABLE);
11494 let result = connection.writable(&mut context, EndpointClient(&mut router));
11495
11496 assert!(
11497 !connection.socket.socket_wants_write(),
11498 "the kernel edge must flush the pending records"
11499 );
11500 assert!(
11501 !matches!(result, MuxResult::Continue),
11502 "a drained GoAway flush must reach the disconnect, got {result:?}"
11503 );
11504 }
11505
11506 // ── The write pass's own flush triple (`finalize_write`) ────────────
11507 //
11508 // `ConnectionH2::finalize_write` ends every write pass with the same
11509 // query / flush / query shape the close sites use, and its decision now
11510 // lives beside theirs in `h2_close::finalize_action`. The two tests below
11511 // are the CALLER half — that `h2.rs` performs the step each answer names
11512 // and wires the inputs to the right parameters. The answers themselves are
11513 // enumerated exhaustively in `h2_close`'s own tables, which no socket can
11514 // reach: `SkipFlush`, `Parked` and `RetainPendingBack` are covered there
11515 // only, because reaching them through `writable()` needs a stream carrying
11516 // response bytes through the scheduler and none of this module's fixtures
11517 // builds one. Same boundary `h2_close`'s `force_disconnect` re-arm branch
11518 // already sits on.
11519
11520 /// The pass flushes once of its own and re-arms when records survive it.
11521 ///
11522 /// Three records, one drained per flush: `writable`'s preamble takes the
11523 /// first, `finalize_write`'s own flush takes the second, and the third is
11524 /// still pending when the post-flush query runs — so the re-arm branch is
11525 /// reached with the socket genuinely still holding data, not by a fixture
11526 /// that can only answer one way.
11527 ///
11528 /// TO SEE THIS RED, either half independently:
11529 /// (a) answer `H2FinalizeTarget::SkipFlush` from the
11530 /// `FinalizeAction::Flush` arm of `finalize_write` (the `SkipFlush`
11531 /// behaviour, which is what a caller that dropped the `socket_write`
11532 /// input would do for every pass). `write_streams` then performs no
11533 /// empty-buffer flush of its own and the flush count assertion fails
11534 /// with `finalize_write must attempt exactly one empty-buffer flush
11535 /// of its own`, `left: 1, right: 2`.
11536 /// (b) empty the `FinalizeAction::ReArm` arm of
11537 /// `finalize_write_after_flush`. The event assertion fails with `the
11538 /// WRITABLE event must be re-signalled`.
11539 /// Measured: `1114 passed; 1 failed` for each half, this test and nothing
11540 /// else. Neither recipe reddens through a production `unreachable!`.
11541 #[test]
11542 fn a_finalized_write_pass_flushes_once_and_re_arms_while_records_survive() {
11543 let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
11544 let (mut connection, _peer) = connection_with_backpressure(&pool, 3, 1, H2State::Header);
11545 let mut context = test_context(&pool);
11546 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
11547
11548 assert!(
11549 connection.socket.socket_wants_write(),
11550 "premise: this harness must report buffered TLS records"
11551 );
11552 // Not the interest bit: `signal_pending_write` touches `event` only,
11553 // and this fixture hands `Ready::WRITABLE` to `ConnectionH2::new` as
11554 // its INTEREST, so an assertion written against interest would be
11555 // reading the fixture's own argument back and could not see the
11556 // re-arm at all.
11557 assert!(
11558 !connection.core.readiness.event.is_writable(),
11559 "premise: the WRITABLE event must start clear so the re-arm below is the only thing that can set it, got {:?}",
11560 connection.core.readiness
11561 );
11562
11563 let result = connection.writable(&mut context, EndpointClient(&mut router));
11564
11565 assert!(
11566 matches!(result, MuxResult::Continue),
11567 "a write pass with records still buffered continues, got {result:?}"
11568 );
11569 assert_eq!(
11570 connection.socket.flushes.get(),
11571 2,
11572 "finalize_write must attempt exactly one empty-buffer flush of its own on top of the preamble's"
11573 );
11574 assert!(
11575 connection.socket.socket_wants_write(),
11576 "premise: one record must survive both flushes, or the post-flush query could only answer one way"
11577 );
11578 assert!(
11579 connection.core.readiness.event.is_writable(),
11580 "the WRITABLE event must be re-signalled so the event loop retries the flush, got {:?}",
11581 connection.core.readiness
11582 );
11583 }
11584
11585 /// A pass that owes nothing withdraws `Ready::WRITABLE` interest.
11586 ///
11587 /// This is the other side of the same decision: no TLS records (a plain
11588 /// `mio::net::TcpStream` takes `socket_wants_write()`'s `false` default),
11589 /// no parked `expect_write`, no bytes written, no queued control frame.
11590 /// Forward progress must come from an external trigger, so the connection
11591 /// relinquishes the bit rather than busy-spinning against the dispatcher.
11592 ///
11593 /// TO SEE THIS RED: delete `self.readiness.interest.remove(Ready::WRITABLE);`
11594 /// from the `FinalizeAction::Quiesce` arm of `finalize_write`, leaving its
11595 /// debug narration in place. The final assertion fails with `a pass that
11596 /// owes nothing must relinquish WRITABLE interest`. The interest bit is
11597 /// asserted here rather than the event bit because withdrawal is what this
11598 /// branch does, and it is set by the test rather than by the fixture —
11599 /// `test_h2_connection` arms `READABLE | HUP | ERROR` only.
11600 #[test]
11601 fn a_write_pass_that_owes_nothing_withdraws_writable_interest() {
11602 let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
11603 let (mut connection, _peer) = test_h2_connection(&pool, None);
11604 let mut context = test_context(&pool);
11605 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
11606
11607 connection.core.state = H2State::Header;
11608 connection.core.readiness.interest.insert(Ready::WRITABLE);
11609
11610 assert!(
11611 !connection.socket.socket_wants_write(),
11612 "premise: a plain TcpStream holds no TLS records, so the readiness policy is reached at all"
11613 );
11614 assert!(
11615 connection.core.stream_table.expect_write().is_none(),
11616 "premise: no parked partial write, or the policy is suppressed"
11617 );
11618
11619 let result = connection.writable(&mut context, EndpointClient(&mut router));
11620
11621 assert!(
11622 matches!(result, MuxResult::Continue),
11623 "an empty write pass continues, got {result:?}"
11624 );
11625 assert!(
11626 !connection.core.readiness.interest.is_writable(),
11627 "a pass that owes nothing must relinquish WRITABLE interest, got {:?}",
11628 connection.core.readiness
11629 );
11630 }
11631
11632 // ── The control-frame drains' own edge-triggered re-arm ─────────────
11633 //
11634 // `ConnectionH2::flush_pending_control_frames` queues the WINDOW_UPDATE
11635 // and RST_STREAM frames behind the ordered output and asks for ONE flush
11636 // (#1604). When that flush stalls, `update_readiness_after_write`
11637 // (`mux/mod.rs`) has just REMOVED the WRITABLE event bit, and the
11638 // `Stalled` answer puts it back when the socket still holds records it
11639 // could not hand to the kernel — unless the kernel refused the write
11640 // (`WouldBlock`): it then owes the socket its next edge, and the re-arm
11641 // would only repeat the refused write (sozu-proxy/sozu#1788,
11642 // `a_refused_goaway_flush_waits_for_the_kernel_edge_then_closes`). The
11643 // two tests below therefore stall the flush on a zero-length write the
11644 // kernel did NOT refuse, the case the re-arm still exists for. Before #1604 each of the two stages had a
11645 // stall tail of its own; the two tests below cover the one that remains.
11646 //
11647 // Until the two tests below existed, nothing in the suite reached either
11648 // stalled branch. Measured by replacing each re-arm with a `panic!` and
11649 // running the whole `sozu-lib` suite against `98745bba`: 1092 passed, 0
11650 // failed, neither panic observed. That is what these tests are for —
11651 // `Readiness::signal_pending_write`'s two `debug_assert!`s only guard a
11652 // path some test actually walks, and a stage that spells the re-arm by
11653 // hand carries no assertion at all.
11654 //
11655 // Neither test can see WHICH spelling a stage uses:
11656 // `readiness.event.insert(Ready::WRITABLE)` and `signal_pending_write()`
11657 // are indistinguishable from outside, by construction — `insert` cannot
11658 // clear a neighbouring bit, so the two are the same state transition.
11659 // What they detect is the re-arm going missing, which strands the
11660 // connection: the serialized frame waits in the output queue and no
11661 // further epoll edge is coming for it.
11662
11663 /// The WINDOW_UPDATE drain re-signals WRITABLE when its flush stalls.
11664 ///
11665 /// TO SEE THIS RED: in `ConnectionH2::handle_control_flush`, replace
11666 /// `H2ControlFlushTarget::Stalled` with
11667 /// `H2ControlFlushTarget::Done(MuxResult::Continue)` — the pass still
11668 /// ends on `Continue`, and the only thing lost is the TLS re-arm
11669 /// `H2Shell::writable` performs for `Stalled`. The last assertion fails
11670 /// with `the stalled WINDOW_UPDATE drain must re-signal the WRITABLE
11671 /// event`: `flush_output_to_socket` cleared the bit on the way in and
11672 /// nothing else on this path sets it. Measured 2026-09-27: this test and
11673 /// its RST_STREAM sibling fail, and no other test of the pair's filter.
11674 #[test]
11675 fn a_stalled_window_update_drain_re_signals_the_writable_event() {
11676 let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
11677 // One record rustls still holds and a kernel that accepts nothing, so
11678 // `socket_wants_write()` answers true for the whole test.
11679 let (mut connection, _peer) = connection_with_backpressure(&pool, 1, 0, H2State::Header);
11680 // The output flush stalls on its first round. A zero-length write
11681 // is what `update_readiness_after_write` reads as a stall, and it
11682 // removes the WRITABLE event bit before the drain stage can re-arm.
11683 // `Continue`, not `WouldBlock`: no kernel edge is owed for it.
11684 connection
11685 .socket
11686 .write_script
11687 .push_back((0, SocketResult::Continue));
11688 let mut context = test_context(&pool);
11689 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
11690
11691 assert!(
11692 connection.socket.socket_wants_write(),
11693 "premise: this harness must report buffered TLS records, or the \
11694 re-arm is gated off and the assertion below could not fail"
11695 );
11696 // Connection-level replenishment: ordinary housekeeping, queued the
11697 // way `handle_frame` queues it.
11698 connection.core.queue_window_update(0, 65_535);
11699 assert!(
11700 connection.core.stream_table.expect_write().is_none(),
11701 "premise: no parked partial write, or the drain stage is skipped"
11702 );
11703
11704 let result = connection.writable(&mut context, EndpointClient(&mut router));
11705
11706 assert!(
11707 matches!(result, MuxResult::Continue),
11708 "a stalled control-frame flush parks the pass and continues, got \
11709 {result:?}"
11710 );
11711 assert!(
11712 !connection.core.output.is_empty(),
11713 "premise: the pass must have taken the STALLED branch of the \
11714 output flush, which leaves the WINDOW_UPDATE queued"
11715 );
11716 assert!(
11717 connection.core.readiness.event.is_writable(),
11718 "the stalled WINDOW_UPDATE drain must re-signal the WRITABLE \
11719 event: under edge-triggered epoll the parked frame has no other \
11720 wake-up, got {:?}",
11721 connection.core.readiness
11722 );
11723 }
11724
11725 /// The RST_STREAM drain re-signals WRITABLE when its flush stalls.
11726 ///
11727 /// The sibling of the WINDOW_UPDATE test above, on the stage beneath it.
11728 /// No WINDOW_UPDATE is queued on purpose: that stage returns early, so
11729 /// queueing one would make this test exercise the wrong drain.
11730 ///
11731 /// TO SEE THIS RED: the `handle_control_flush` mutation described on the
11732 /// WINDOW_UPDATE test above. The last assertion fails with `the stalled
11733 /// RST_STREAM drain must re-signal the WRITABLE event`. Measured
11734 /// 2026-09-27.
11735 #[test]
11736 fn a_stalled_rst_stream_drain_re_signals_the_writable_event() {
11737 let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
11738 let (mut connection, _peer) = connection_with_backpressure(&pool, 1, 0, H2State::Header);
11739 connection
11740 .socket
11741 .write_script
11742 .push_back((0, SocketResult::Continue));
11743 let mut context = test_context(&pool);
11744 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
11745
11746 assert!(
11747 connection.socket.socket_wants_write(),
11748 "premise: this harness must report buffered TLS records, or the \
11749 re-arm is gated off and the assertion below could not fail"
11750 );
11751 // One RST, well under the pending-queue bound, so the overflow check
11752 // above this stage does not escalate to GOAWAY(ENHANCE_YOUR_CALM) and
11753 // swallow the drain.
11754 assert!(
11755 connection
11756 .core
11757 .enqueue_rst(1, H2Error::Cancel, RstOrigin::Local)
11758 .is_none(),
11759 "premise: a single RST must not trip the flood detector"
11760 );
11761 assert!(
11762 connection
11763 .core
11764 .flow_control
11765 .pending_window_updates_is_empty(),
11766 "premise: no queued WINDOW_UPDATE, or the stage above returns \
11767 first and this test exercises the wrong drain"
11768 );
11769
11770 let result = connection.writable(&mut context, EndpointClient(&mut router));
11771
11772 assert!(
11773 matches!(result, MuxResult::Continue),
11774 "a stalled control-frame flush parks the pass and continues, got \
11775 {result:?}"
11776 );
11777 assert!(
11778 !connection.core.output.is_empty(),
11779 "premise: the pass must have taken the STALLED branch of the \
11780 output flush, which leaves the RST_STREAM queued"
11781 );
11782 assert!(
11783 connection.core.readiness.event.is_writable(),
11784 "the stalled RST_STREAM drain must re-signal the WRITABLE event: \
11785 under edge-triggered epoll the parked frame has no other wake-up, \
11786 got {:?}",
11787 connection.core.readiness
11788 );
11789 }
11790
11791 /// The kernel-refused sibling of the two drain tests above: the output
11792 /// flush stalls on `(0, WouldBlock)`, and the `Stalled` arm must leave
11793 /// WRITABLE to the kernel's next edge instead of re-raising it. A
11794 /// synthetic edge here made `Mux::ready_inner` repeat the refused flush
11795 /// on every inner iteration (sozu-proxy/sozu#1788).
11796 ///
11797 /// TO SEE THIS RED: drop `self.note_write_status(status);` from
11798 /// `ConnectionH2::consume_output_flush`. The refusal is then never
11799 /// recorded and `ensure_tls_flushed` re-raises the event for both drains.
11800 #[test]
11801 fn a_refused_control_frame_drain_leaves_writable_to_the_kernel() {
11802 for drain in ["WINDOW_UPDATE", "RST_STREAM"] {
11803 let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
11804 let (mut connection, _peer) =
11805 connection_with_backpressure(&pool, 1, 0, H2State::Header);
11806 connection
11807 .socket
11808 .write_script
11809 .push_back((0, SocketResult::WouldBlock));
11810 let mut context = test_context(&pool);
11811 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
11812 if drain == "WINDOW_UPDATE" {
11813 connection.core.queue_window_update(0, 65_535);
11814 } else {
11815 assert!(
11816 connection
11817 .core
11818 .enqueue_rst(1, H2Error::Cancel, RstOrigin::Local)
11819 .is_none(),
11820 "premise: a single RST must not trip the flood detector"
11821 );
11822 }
11823 connection.core.readiness.event.insert(Ready::WRITABLE);
11824
11825 let result = connection.writable(&mut context, EndpointClient(&mut router));
11826
11827 assert!(
11828 matches!(result, MuxResult::Continue),
11829 "a refused {drain} drain parks the pass and continues, got {result:?}"
11830 );
11831 assert!(
11832 !connection.core.output.is_empty(),
11833 "premise: the {drain} flush must have stalled, leaving the frame queued"
11834 );
11835 assert!(
11836 connection.socket.socket_wants_write(),
11837 "premise: rustls must still hold records, or the re-arm is gated off"
11838 );
11839 assert!(
11840 !connection.core.readiness.event.is_writable(),
11841 "a {drain} drain the kernel refused must leave WRITABLE to the \
11842 kernel's next edge, got {:?}",
11843 connection.core.readiness
11844 );
11845 }
11846 }
11847
11848 /// A stream write the kernel refuses, while rustls still holds records,
11849 /// ends the pass without a synthetic WRITABLE. The pass already wrote
11850 /// through `socket_write_vectored`, so `finalize_write` skips its own
11851 /// flush and decides on the refusal `handle_write` recorded: the preamble
11852 /// flush before it answered `Continue`, so without that record the
11853 /// `ReArm` would re-raise the event.
11854 ///
11855 /// TO SEE THIS RED: drop `self.note_write_status(status);` from
11856 /// `ConnectionH2::handle_write`.
11857 #[test]
11858 fn a_refused_stream_write_leaves_writable_to_the_kernel() {
11859 let pool = make_pool_for_invariant_16();
11860 let (mut connection, mut context, gid, _peer) = writable_fixture(
11861 &pool,
11862 &[FIRST_BLOCK, SECOND_BLOCK],
11863 &[(0, SocketResult::WouldBlock)],
11864 );
11865 connection.socket.pending.set(1);
11866 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
11867 assert_prepare_gate_is_shut(&context, gid);
11868 connection.core.readiness.event.insert(Ready::WRITABLE);
11869
11870 let result = connection.writable(&mut context, EndpointClient(&mut router));
11871
11872 assert!(
11873 matches!(result, MuxResult::Continue),
11874 "a refused stream write continues, got {result:?}"
11875 );
11876 assert_eq!(
11877 connection.socket.vectored_calls, 1,
11878 "premise: the pass must have reached the stream write"
11879 );
11880 assert!(
11881 connection.socket.socket_wants_write(),
11882 "premise: rustls must still hold records"
11883 );
11884 assert!(
11885 !connection.core.readiness.event.is_writable(),
11886 "a stream write the kernel refused must leave WRITABLE to the \
11887 kernel's next edge, got {:?}",
11888 connection.core.readiness
11889 );
11890 }
11891
11892 // ── The vectored write loop: a partial write that reports WouldBlock ──
11893 //
11894 // First, which shape each neighbour targets, because #1454 asks for that
11895 // and the shapes are near-identical. `h2.rs` has FOUR `socket_write(&[])`
11896 // sites. Three are the `query / socket_write(&[]) / query` TRIPLE, where
11897 // the middle call discards both `size` and `status` and only the second
11898 // query says whether the flush landed:
11899 //
11900 // - `finalize_write`, the BeforeFlush/AfterFlush pair — covered by
11901 // `a_finalized_write_pass_flushes_once_and_re_arms_while_records_survive`.
11902 // - the `H2State::GoAway` arm of `dispatch_writable_state`, whose flush
11903 // `writable` performs for it — covered by
11904 // `a_flush_that_does_not_drain_keeps_the_connection_open` and
11905 // `a_flush_that_succeeds_closes_within_one_writable_call`.
11906 // - `writable`'s preamble paired with the `error_close_action` query of
11907 // `dispatch_writable_state`'s `(H2State::Error, Position::Server)`
11908 // arm. The preamble half is asserted by the two GoAway tests above,
11909 // and #1454's
11910 // `a_rustls_frontend_in_error_state_stays_open_until_its_records_drain`
11911 // drives the second query over the production handler — this line
11912 // said **Uncovered** until that test existed, and it was already
11913 // wrong before this changeset renamed what it points at.
11914 //
11915 // The fourth, in `flush_output_buffer`, is NOT a triple: it keeps the
11916 // `status` the flush returned instead of re-querying. Also uncovered.
11917 //
11918 // The tests below target none of those. They target **the vectored loop
11919 // the triples bracket** — the pre-image's `while !kawa.out.is_empty()`,
11920 // now `poll_write_target`'s round-again over `H2WritePhase::Flush` — so
11921 // naming a triple for them would be naming the wrong shape.
11922 //
11923 // What had never run against production code: a `socket_write_vectored`
11924 // that moves bytes AND reports `WouldBlock`. `update_readiness` (`mux/mod.rs`)
11925 // classifies a pass as stalled **iff `size == 0`** — `status` only clears
11926 // the event bit — so `size > 0 && status == WouldBlock` is NOT a stall and
11927 // the loop must go round again. A write machine that treated
11928 // `status != Continue` as a pass terminator would drop that second write
11929 // and truncate the response, and would pass every test that existed.
11930 //
11931 // Why no test could see it. `BackpressuredTlsSocket::socket_write_vectored`
11932 // delegated to a real loopback socket, and a kernel that blocks answers
11933 // `(0, WouldBlock)` — never `(size > 0, WouldBlock)`. Only `FrontRustls`
11934 // returns that pair, and it does so structurally: `buffered_size` counts
11935 // plaintext `session.writer().write(..)` accepted, while `can_write` goes
11936 // false when `write_tls` blocks against the kernel. Two independent
11937 // quantities, one return value.
11938 //
11939 // `h2_transmit`'s `qc_partial_writes_preserve_the_byte_stream` does drive
11940 // partial writes, but through `drive`, a loop written in its own test
11941 // module that calls neither `ConnectionH2::handle_write` nor
11942 // `update_readiness_after_write`; its `WritePlan` carries an accept COUNT
11943 // and no `SocketResult` at all. A mirror cannot constrain the thing it
11944 // mirrors, and that plan cannot express this pair in the first place.
11945
11946 /// What the pre-image's `FlushOutcome` said about one stream's flush,
11947 /// re-derived here because production no longer has the type.
11948 ///
11949 /// The inversion deleted it along with `flush_stream_out`: the flush loop
11950 /// is now the `Transmit`/`handle_write` round trip, whose exit condition
11951 /// is read by `poll_write_target` from `kawa.out` and `pass.stalled`
11952 /// rather than reported as a return value. `Drained` is therefore derived
11953 /// from the queue being empty — the pre-image's own
11954 /// `while !kawa.out.is_empty()` exit condition, verbatim — and NOT from
11955 /// `expect_write` being parked. The distinction matters: the park has its
11956 /// own dedicated tests below, and deriving the outcome from it would make
11957 /// this classification move under edits those tests already cover.
11958 ///
11959 /// **It is therefore the same fact as the `out_is_empty` element of
11960 /// [`drive_flush_stream_out`]'s tuple, and the three tests below assert
11961 /// both.** Stated rather than removed: their bodies are unchanged from
11962 /// before the inversion, which is what makes them evidence that spans it,
11963 /// and the alternatives were measured worse. Deriving this from
11964 /// `expect_write` would couple it to the park, which the stall tests
11965 /// already pin — the double-assertion would become a second test reddening
11966 /// under the `set_expect_write` recipe. Deriving it from the WRITABLE
11967 /// event bit cannot work either: `update_readiness` clears that bit on
11968 /// every non-`Continue` round, so a drained two-round pass and a stalled
11969 /// one leave it identical.
11970 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
11971 enum FlushOutcome {
11972 /// All queued bytes were drained to the socket.
11973 Drained,
11974 /// The socket blocked before the queue was drained.
11975 Stalled,
11976 }
11977
11978 /// Drive one write pass over `blocks` with `script` as the socket's
11979 /// answers, returning what the flush did.
11980 ///
11981 /// Retargeted by the poll/handle inversion. There is no associated
11982 /// function left to call: the loop this drives is spread across
11983 /// `poll_write_target`'s round-again, the shell's gather / write / confirm
11984 /// and `handle_write`, and the smallest production seam that contains all
11985 /// three is `write_streams`. It is called directly rather than through
11986 /// `writable()` so that the preamble's pending-control-frame flush and TLS
11987 /// flush stay out of the measurement; `pending = 0` keeps
11988 /// `socket_wants_write()` false for the whole pass, and nothing below
11989 /// asserts on time, so skipping `writable`'s `self.now = context.now` is
11990 /// inert.
11991 ///
11992 /// What is unchanged is the point of the harness: `vectored_script` still
11993 /// lets the `(size, status)` pair be CHOSEN rather than negotiated with a
11994 /// kernel, which is the only way `(size > 0, WouldBlock)` is expressible
11995 /// at all.
11996 fn drive_flush_stream_out(
11997 pool: &Rc<RefCell<Pool>>,
11998 blocks: &[&'static [u8]],
11999 script: &[(usize, SocketResult)],
12000 ) -> (FlushOutcome, usize, usize, bool, Readiness) {
12001 let (mut connection, mut context, gid, _peer) = writable_fixture(pool, blocks, script);
12002 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
12003
12004 connection.write_streams(&mut context, EndpointClient(&mut router));
12005
12006 let out_is_empty = context.streams[gid].back.out.is_empty();
12007 (
12008 if out_is_empty {
12009 FlushOutcome::Drained
12010 } else {
12011 FlushOutcome::Stalled
12012 },
12013 context.streams[gid].metrics.bout,
12014 connection.socket.vectored_calls,
12015 out_is_empty,
12016 connection.core.readiness.clone(),
12017 )
12018 }
12019
12020 /// Bytes the two blocks below carry, and the whole response as far as
12021 /// this stream is concerned: ONE DATA frame on stream 1, split across two
12022 /// blocks, 22 + 21 bytes. A stream's `kawa.out` only ever holds whole H2
12023 /// frames, and since #1604 the write pass relies on it to find where the
12024 /// frame a partial write cut ends (`h2_transmit::frame_tail`).
12025 const FIRST_BLOCK: &[u8] = b"\x00\x00\x22\x00\x00\x00\x00\x00\x01HTTP/2 respon";
12026 const SECOND_BLOCK: &[u8] = b" and its continuation";
12027 const TOTAL_QUEUED: usize = FIRST_BLOCK.len() + SECOND_BLOCK.len();
12028
12029 /// A write that moved bytes but reported `WouldBlock` does NOT end the
12030 /// pass: the loop goes round again and delivers the rest.
12031 ///
12032 /// This is the truncation vector of #1454 at the vectored loop — not at
12033 /// any of the three `socket_wants_write()` triples. `FrontRustls` returns
12034 /// `(buffered_size, WouldBlock)` with `buffered_size > 0` whenever rustls
12035 /// took plaintext off our hands while the kernel was full, and
12036 /// `update_readiness` deliberately calls that NOT stalled. A machine that
12037 /// terminated the pass on `status != Continue` would leave
12038 /// `SECOND_BLOCK` queued and send the response short.
12039 ///
12040 /// TO SEE THIS RED: in `handle_write`, change
12041 /// `pass.stalled = update_readiness_after_write(size, status, &mut self.readiness);`
12042 /// to
12043 /// `pass.stalled = update_readiness_after_write(size, status, &mut self.readiness)
12044 /// || !matches!(status, SocketResult::Continue);`
12045 /// — the pass-terminator shape the `write_streams` inversion could
12046 /// introduce, and the reason `SocketResult` is confined to this one
12047 /// statement and this one parameter. Measured on that mutation: this test
12048 /// fails on its own first assertion, `a vectored write that moved bytes
12049 /// but reported WouldBlock must not end the pass`, with
12050 /// `left: 5, right: 43`. It is one of THREE failures, the siblings being
12051 /// `a_partial_write_reporting_would_block_continues_the_pass_through_writable`
12052 /// and `a_yielded_incremental_stream_is_prepared_once_per_pass`; every
12053 /// other test in the suite passes, including both controls below. No
12054 /// production `debug_assert!` fires: every scripted size is clamped to the
12055 /// gather's offer.
12056 #[test]
12057 fn a_partial_write_reporting_would_block_continues_the_pass() {
12058 let pool = make_pool_for_invariant_16();
12059 // Round 1 accepts 5 of the 43 offered and reports WouldBlock; round 2
12060 // takes everything still queued and reports Continue.
12061 // The event bit is deliberately not read here: `update_readiness`
12062 // removes WRITABLE on the WouldBlock round and the Continue round does
12063 // not put it back, so its final value is identical under the correct
12064 // and the broken machine. An assertion on it could not fail.
12065 let (outcome, bytes_written, vectored_calls, out_is_empty, _readiness) =
12066 drive_flush_stream_out(
12067 &pool,
12068 &[FIRST_BLOCK, SECOND_BLOCK],
12069 &[
12070 (5, SocketResult::WouldBlock),
12071 (TOTAL_QUEUED, SocketResult::Continue),
12072 ],
12073 );
12074
12075 assert_eq!(
12076 bytes_written, TOTAL_QUEUED,
12077 "a vectored write that moved bytes but reported WouldBlock must \
12078 not end the pass: the loop must go round again and deliver the \
12079 remaining bytes, or the response is truncated"
12080 );
12081 assert_eq!(
12082 vectored_calls, 2,
12083 "the loop must issue a SECOND socket_write_vectored after the \
12084 partial write, not settle for the first"
12085 );
12086 assert!(
12087 out_is_empty,
12088 "every queued block must be consumed once the pass drains"
12089 );
12090 assert!(
12091 matches!(outcome, FlushOutcome::Drained),
12092 "a queue emptied within the pass is Drained, got {outcome:?}"
12093 );
12094 }
12095
12096 /// A write the socket takes whole, in one round, reporting `Continue`.
12097 ///
12098 /// **This test does not discriminate, and that is its job.** It is the
12099 /// `status == Continue` path — the one every existing test already takes,
12100 /// because a real loopback socket with room answers exactly this. It
12101 /// passes against the correct loop AND against the pass-terminator
12102 /// mutation in the test above, since a terminator keyed on
12103 /// `status != Continue` never fires here. Kept as the standing
12104 /// demonstration that coverage of this shape proves nothing about the
12105 /// truncation vector: the suite was full of it and #1454 was open anyway.
12106 #[test]
12107 fn a_fully_accepted_write_drains_in_one_round() {
12108 let pool = make_pool_for_invariant_16();
12109 let (outcome, bytes_written, vectored_calls, out_is_empty, _readiness) =
12110 drive_flush_stream_out(
12111 &pool,
12112 &[FIRST_BLOCK, SECOND_BLOCK],
12113 &[(TOTAL_QUEUED, SocketResult::Continue)],
12114 );
12115
12116 assert_eq!(
12117 bytes_written, TOTAL_QUEUED,
12118 "a socket that takes the whole offer delivers the whole queue"
12119 );
12120 assert_eq!(
12121 vectored_calls, 1,
12122 "nothing is left to write, so the loop must not go round again"
12123 );
12124 assert!(out_is_empty, "the queue must be empty after a full accept");
12125 assert!(
12126 matches!(outcome, FlushOutcome::Drained),
12127 "a fully accepted offer is Drained, got {outcome:?}"
12128 );
12129 }
12130
12131 /// A write that moves NO bytes stalls the pass and drops WRITABLE.
12132 ///
12133 /// The other side of `update_readiness`'s single decision: `size == 0` is
12134 /// the stall, whatever the status. Together with the partial-write test
12135 /// above this pins that the discriminator is the SIZE and not the status —
12136 /// both rounds there reported a non-`Continue` status, and only this one
12137 /// stops.
12138 ///
12139 /// Like `a_fully_accepted_write_drains_in_one_round`, this passes under
12140 /// the pass-terminator mutation too: `(0, WouldBlock)` stalls either way.
12141 /// The size arm is the covered one; the partial arm was not.
12142 ///
12143 /// TO SEE THIS RED: in `update_readiness` (`mux/mod.rs`), change the
12144 /// `else` arm's trailing `true` to `false`, so no size ever reports a
12145 /// stall. Measured: the first assertion fails with `a write that moved no
12146 /// bytes must stall the pass, got Drained`. It does not hang — the script
12147 /// here is a single round, so the loop re-enters, finds the script spent,
12148 /// delegates to the live loopback socket and drains.
12149 #[test]
12150 fn a_write_that_moves_no_bytes_stalls_the_pass() {
12151 let pool = make_pool_for_invariant_16();
12152 let (outcome, bytes_written, vectored_calls, out_is_empty, readiness) =
12153 drive_flush_stream_out(
12154 &pool,
12155 &[FIRST_BLOCK, SECOND_BLOCK],
12156 &[(0, SocketResult::WouldBlock)],
12157 );
12158
12159 assert!(
12160 matches!(outcome, FlushOutcome::Stalled),
12161 "a write that moved no bytes must stall the pass, got {outcome:?}"
12162 );
12163 assert_eq!(
12164 vectored_calls, 1,
12165 "a stalled pass must not retry the socket within the same pass"
12166 );
12167 assert_eq!(bytes_written, 0, "a stalled write delivered nothing");
12168 assert!(
12169 !out_is_empty,
12170 "the undelivered blocks must stay queued for the next pass, or \
12171 they are lost"
12172 );
12173 assert!(
12174 !readiness.event.is_writable(),
12175 "a stalled pass must clear the WRITABLE event so the pass is not \
12176 re-entered before the socket says it can take more, got {readiness:?}"
12177 );
12178 }
12179
12180 // ── The two write-path sites `writable()` had never reached ──────────
12181 //
12182 // A stalled flush has ONE producer — the `pass.stalled` assignment in
12183 // `handle_write`, `FlushOutcome::Stalled` before the inversion — and
12184 // exactly two consumers, and both consumers were dead code as far as the
12185 // suite was concerned:
12186 //
12187 // - the resume path's `H2WriteTarget::Done(MuxResult::Continue)`, taken
12188 // in `H2WritePhase::Resume` for the stream `expect_write` parked, whose
12189 // transmits carry `debug_site = 2`;
12190 // - `H2WritePhase::Flush`'s
12191 // `set_expect_write(Some(H2StreamId::Other { .. }))` followed by the
12192 // move to `H2WritePhase::End` — `break 'outer` before the inversion.
12193 //
12194 // Measured on `38fe5851`, against the linear pre-image these tests were
12195 // written for: a `panic!` planted at BOTH consumers at once still leaves
12196 // the whole suite green — neither is reached by any test. A `panic!` on
12197 // the `return FlushOutcome::Stalled` they both read fails exactly ONE
12198 // test, `a_write_that_moves_no_bytes_stalls_the_pass` above, which called
12199 // `flush_stream_out` directly and never entered `write_streams` at all.
12200 // A `panic!` on the first line of the resume block leaves the suite green
12201 // as well: `debug_site = 2` had no caller in it. Those measurements are a
12202 // record of the revision named, not a claim about this one — the three
12203 // tests above now reach `write_streams` too, since `flush_stream_out` is
12204 // gone and there is no associated function left to call.
12205 //
12206 // No absolute pass count appears in this block or in any recipe below, on
12207 // purpose. A count of the whole `sozu-lib` suite is falsified by a test
12208 // added anywhere in the crate, and nothing checks it —
12209 // `check_doc_citations.py` reads only `doc/` and `**/LIFECYCLE.md`. The
12210 // failing test NAMES and the `left:`/`right:` values are what survive.
12211 //
12212 // Why the gap existed. A stall needs `size == 0` against a non-empty
12213 // `kawa.out`. The two `BackpressuredTlsSocket` `writable()` tests above
12214 // sit in `H2State::GoAway` and take that arm of `writable`, never
12215 // `write_streams`; the two after them do reach `write_streams`, but with
12216 // an empty stream table, so the scheduler loop visits no stream. Every
12217 // other `writable()` test writes to a live loopback socket with room, and
12218 // a kernel with room never answers 0.
12219 //
12220 // The missing step was never the socket — `vectored_script` already
12221 // scripts any `(size, status)` pair, including ones no kernel produces.
12222 // It was REGISTERING a stream that carries response bytes, so the
12223 // scheduler reaches it through `writable()`. That is what
12224 // `writable_fixture` below adds. It is now what `drive_flush_stream_out`
12225 // builds on too: the difference left between the two is only that this
12226 // group enters through `writable()` and that one through `write_streams`
12227 // directly.
12228
12229 /// The stream id every test below registers. Odd, because a server's peer
12230 /// opens odd-numbered streams (RFC 9113 §5.1.1).
12231 const REGISTERED_STREAM_ID: StreamId = 1;
12232
12233 /// A server connection in `H2State::Header` plus a context holding ONE
12234 /// registered stream whose response buffer already carries `blocks`, with
12235 /// the socket's `(size, status)` answers scripted.
12236 ///
12237 /// `pending = 0` keeps `socket_wants_write()` false for the whole pass, so
12238 /// `writable`'s preamble flush and both of `finalize_write`'s TLS branches
12239 /// stay out of the way: the readiness bits these tests read are decided by
12240 /// the write pass alone.
12241 ///
12242 /// The script is assigned to the field directly: a builder taking `self`
12243 /// by value cannot be applied to a handler already moved into
12244 /// `ConnectionH2::new`.
12245 ///
12246 /// The peer end of the loopback pair is returned for the caller to hold:
12247 /// dropping it would close the connection under the socket being tested.
12248 fn writable_fixture(
12249 pool: &Rc<RefCell<Pool>>,
12250 blocks: &[&'static [u8]],
12251 script: &[(usize, SocketResult)],
12252 ) -> (
12253 H2Shell<BackpressuredTlsSocket>,
12254 Context<TestListener>,
12255 GlobalStreamId,
12256 std::net::TcpStream,
12257 ) {
12258 let (mut connection, peer) = connection_with_backpressure(pool, 0, 0, H2State::Header);
12259 connection.socket.vectored_script = script.iter().copied().collect();
12260 let mut context = test_context(pool);
12261 let gid = context
12262 .create_stream(Ulid::generate(), 1 << 16)
12263 .expect("test context must create a stream");
12264 connection
12265 .core
12266 .stream_table
12267 .register(REGISTERED_STREAM_ID, gid, connection.core.now);
12268 for block in blocks {
12269 context.streams[gid]
12270 .back
12271 .out
12272 .push_back(kawa::OutBlock::Store(kawa::Store::Static(block)));
12273 }
12274 (connection, context, gid, peer)
12275 }
12276
12277 /// A fresh `Stream`'s response buffer is in `ParsingPhase::StatusLine`,
12278 /// which `is_main_phase()` answers `false` for, so the `write_streams`
12279 /// prepare gate stays shut and the pass is the flush and nothing else.
12280 ///
12281 /// Every `writable_fixture` test rests on that, because each one counts
12282 /// either bytes or socket calls and a prepare would add both. Asserted as
12283 /// a premise rather than assumed: if a future `Stream::new` started life
12284 /// in `Body`, those tests would keep passing while measuring something
12285 /// else. `a_yielded_incremental_stream_is_prepared_once_per_pass` below
12286 /// deliberately opens the gate instead, and builds its own fixture.
12287 fn assert_prepare_gate_is_shut(context: &Context<TestListener>, gid: GlobalStreamId) {
12288 let back = &context.streams[gid].back;
12289 assert!(
12290 !back.is_main_phase() && !back.is_terminated() && !back.is_error(),
12291 "premise: the prepare gate must stay shut so this pass is a pure \
12292 flush, got parsing_phase {:?}",
12293 back.parsing_phase
12294 );
12295 }
12296
12297 /// A write that moved bytes but reported `WouldBlock` does not end the
12298 /// pass — driven through `writable()`, over a registered stream.
12299 ///
12300 /// `a_partial_write_reporting_would_block_continues_the_pass` above pins
12301 /// the same contract one layer down, entering at `write_streams` and
12302 /// reading the flush's own outcome. This one pins that the SCHEDULER
12303 /// honours it: the `(size > 0, WouldBlock)` answer has to travel back out
12304 /// through `poll_write_target`'s round-again at `H2WritePhase::Flush`,
12305 /// past the `set_expect_write` park, and through `finalize_write` without
12306 /// anything on the way deciding the pass is over. A write machine that
12307 /// treated `status != Continue` as a pass terminator would park
12308 /// `expect_write`, leave `SECOND_BLOCK` queued, and truncate the response.
12309 ///
12310 /// TO SEE THIS RED: in `handle_write`, change
12311 /// `pass.stalled = update_readiness_after_write(size, status, &mut self.readiness);`
12312 /// to
12313 /// `pass.stalled = update_readiness_after_write(size, status, &mut self.readiness)
12314 /// || !matches!(status, SocketResult::Continue);`
12315 /// Measured: this test fails on its own first assertion, `every queued
12316 /// byte must reach the socket within the pass`, with `left: 5, right: 43`.
12317 /// That edit also reddens two siblings, so THREE tests fail and not one:
12318 /// `a_partial_write_reporting_would_block_continues_the_pass`, the same
12319 /// contract on the direct path, documenting the same recipe (`left: 5,
12320 /// right: 43`); and
12321 /// `a_yielded_incremental_stream_is_prepared_once_per_pass` below, whose
12322 /// first stream stalls on its `WouldBlock` round so the pass goes to
12323 /// `H2WritePhase::End` before the second stream is ever prepared
12324 /// (`left: 16384, right: 32768`). Three witnesses to one truncation vector, at three different
12325 /// depths — not a duplicate. No production `debug_assert!` fires: every
12326 /// scripted size is clamped to the gather's offer.
12327 #[test]
12328 fn a_partial_write_reporting_would_block_continues_the_pass_through_writable() {
12329 let pool = make_pool_for_invariant_16();
12330 let (mut connection, mut context, gid, _peer) = writable_fixture(
12331 &pool,
12332 &[FIRST_BLOCK, SECOND_BLOCK],
12333 &[
12334 (5, SocketResult::WouldBlock),
12335 (TOTAL_QUEUED, SocketResult::Continue),
12336 ],
12337 );
12338 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
12339
12340 assert_eq!(
12341 connection.core.stream_table.expect_write(),
12342 None,
12343 "premise: no parked write, so the pass takes the main loop and not \
12344 the resume path"
12345 );
12346 assert_prepare_gate_is_shut(&context, gid);
12347
12348 let result = connection.writable(&mut context, EndpointClient(&mut router));
12349
12350 assert_eq!(
12351 context.streams[gid].metrics.bout, TOTAL_QUEUED,
12352 "every queued byte must reach the socket within the pass: a write \
12353 that moved bytes under WouldBlock is not a stall, so the loop must \
12354 go round again instead of ending the pass short"
12355 );
12356 assert_eq!(
12357 connection.socket.vectored_calls, 2,
12358 "the pass must issue a SECOND socket_write_vectored after the \
12359 partial write, not settle for the first"
12360 );
12361 assert!(
12362 context.streams[gid].back.out.is_empty(),
12363 "every queued block must be consumed once the pass drains"
12364 );
12365 assert_eq!(
12366 connection.core.stream_table.expect_write(),
12367 None,
12368 "a pass that drained must not park expect_write: parking here \
12369 would defer a stream that owes nothing to the next writable tick"
12370 );
12371 assert!(
12372 matches!(result, MuxResult::Continue),
12373 "a drained write pass continues, got {result:?}"
12374 );
12375 }
12376
12377 /// A write pass that transmits allocates nothing once the connection is
12378 /// warm: the vectored descriptors live in `H2Shell::io_slices`, which
12379 /// keeps its capacity from one pass to the next instead of being built
12380 /// and freed by every `write_streams` call.
12381 ///
12382 /// The first pass is the warm-up and may grow the scratch; the second
12383 /// queues the same two blocks again and is the measurement. Both go
12384 /// through the loopback socket (empty script), so the write is a real
12385 /// `writev(2)` and the count covers the whole gather / write / confirm
12386 /// round trip plus `poll_write_target` and `handle_write` around it.
12387 ///
12388 /// TO SEE THIS RED: in `H2Shell::write_streams`, declare
12389 /// `let mut io_slices: Vec<IoSlice<'static>> = Vec::new();` above the
12390 /// loop and hand it to `gather`, the write and `confirm` in place of
12391 /// `self.io_slices`. Measured: this test fails with
12392 /// `left: 2, right: 1` under `cargo test`, and `left: 1, right: 0`
12393 /// under `cargo test --release` — one allocation per transmitting pass.
12394 #[test]
12395 fn a_warm_write_pass_allocates_nothing() {
12396 let pool = make_pool_for_invariant_16();
12397 let (mut connection, mut context, gid, _peer) =
12398 writable_fixture(&pool, &[FIRST_BLOCK, SECOND_BLOCK], &[]);
12399 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
12400 assert_prepare_gate_is_shut(&context, gid);
12401
12402 connection.write_streams(&mut context, EndpointClient(&mut router));
12403 assert!(
12404 context.streams[gid].back.out.is_empty(),
12405 "premise: the warm-up pass must drain, or the measured pass would \
12406 start from a different state"
12407 );
12408 for block in [FIRST_BLOCK, SECOND_BLOCK] {
12409 context.streams[gid]
12410 .back
12411 .out
12412 .push_back(kawa::OutBlock::Store(kawa::Store::Static(block)));
12413 }
12414 let calls_before = connection.socket.vectored_calls;
12415
12416 let before = crate::test_allocations::allocations();
12417 connection.write_streams(&mut context, EndpointClient(&mut router));
12418 let allocations = crate::test_allocations::allocations() - before;
12419
12420 assert!(
12421 connection.socket.vectored_calls > calls_before,
12422 "premise: the measured pass must reach socket_write_vectored"
12423 );
12424 assert!(
12425 context.streams[gid].back.out.is_empty(),
12426 "premise: the measured pass must drain the stream"
12427 );
12428 // `finalize_write`'s `Quiesce` arm formats a `DebugEvent::Str` under
12429 // `#[cfg(debug_assertions)]` only; a release build does not compile
12430 // it. Everything else the pass does must be allocation-free.
12431 let debug_instrumentation = usize::from(cfg!(debug_assertions));
12432 assert_eq!(
12433 allocations, debug_instrumentation,
12434 "a warm transmitting write pass must not allocate"
12435 );
12436 }
12437
12438 /// A zero-byte write parks `expect_write` on the stalled stream and ends
12439 /// the pass, so the next tick resumes exactly it.
12440 ///
12441 /// This is `poll_write_target`'s stall consumer — the
12442 /// `set_expect_write(Some(write_stream));` plus the move to
12443 /// `H2WritePhase::End` in the `H2WritePhase::Flush` arm, `break 'outer`
12444 /// before the inversion. Without the park, the next `writable()` would
12445 /// re-enter the scheduler from the top and re-run the priority ordering
12446 /// for a stream that is merely waiting on the socket, and `finalize_write`
12447 /// would read `expect_write_parked == false` and fall through to
12448 /// `Quiesce`, stripping `Ready::WRITABLE` from a connection that has bytes
12449 /// to deliver.
12450 ///
12451 /// TO SEE THIS RED: delete the
12452 /// `self.stream_table.set_expect_write(Some(write_stream));` statement
12453 /// from that arm, keeping the `pass.phase = H2WritePhase::End;` that
12454 /// follows it. Measured: this test fails on its own first
12455 /// assertion, `a stalled stream must park expect_write so the next pass
12456 /// resumes it`, with `left: None, right: Some(Other { id: 1, gid: 0 })`,
12457 /// and it is the ONLY failure in the suite.
12458 #[test]
12459 fn a_stalled_stream_parks_expect_write_for_the_next_pass() {
12460 let pool = make_pool_for_invariant_16();
12461 let (mut connection, mut context, gid, _peer) = writable_fixture(
12462 &pool,
12463 &[FIRST_BLOCK, SECOND_BLOCK],
12464 &[(0, SocketResult::WouldBlock)],
12465 );
12466 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
12467
12468 assert_eq!(
12469 connection.core.stream_table.expect_write(),
12470 None,
12471 "premise: nothing parked on entry, so any park below is this \
12472 pass's doing"
12473 );
12474 assert_prepare_gate_is_shut(&context, gid);
12475
12476 let result = connection.writable(&mut context, EndpointClient(&mut router));
12477
12478 assert_eq!(
12479 connection.core.stream_table.expect_write(),
12480 Some(H2StreamId::Other {
12481 id: REGISTERED_STREAM_ID,
12482 gid
12483 }),
12484 "a stalled stream must park expect_write so the next pass resumes \
12485 it instead of re-running the scheduler over a socket-blocked stream"
12486 );
12487 assert!(
12488 !context.streams[gid].back.out.is_empty(),
12489 "the undelivered blocks must stay queued for the next pass, or \
12490 they are lost"
12491 );
12492 assert_eq!(
12493 connection.socket.vectored_calls, 1,
12494 "a stalled pass must not retry the socket within the same pass"
12495 );
12496 assert_eq!(
12497 context.streams[gid].metrics.bout, 0,
12498 "a stalled write delivered nothing"
12499 );
12500 assert!(
12501 connection.core.readiness.interest.is_writable(),
12502 "a parked pass leaves every readiness bit alone — stripping \
12503 WRITABLE here would strand the parked stream, got {:?}",
12504 connection.core.readiness
12505 );
12506 assert!(
12507 matches!(result, MuxResult::Continue),
12508 "a parked write pass continues, got {result:?}"
12509 );
12510 }
12511
12512 /// A stalled resume returns before the scheduler pass begins.
12513 ///
12514 /// This is the OTHER stall consumer — `H2WritePhase::Resume`'s
12515 /// `if pass.stalled { return H2WriteTarget::Done(MuxResult::Continue); }`.
12516 /// The park it leaves standing is the point: falling through would clear
12517 /// `expect_write` on a stream the socket has just refused, and then run
12518 /// the whole priority ordering, converter pass and census for a
12519 /// connection whose socket is known to be full.
12520 ///
12521 /// The stream is registered even though the resume path reads its gid
12522 /// straight out of `expect_write` and needs no registration. That is what
12523 /// makes `vectored_calls` a witness: with the stream in the table, a pass
12524 /// that fell through into the scheduler phases would reach it again and
12525 /// issue a second `socket_write_vectored`.
12526 ///
12527 /// TO SEE THIS RED: delete the
12528 /// `return H2WriteTarget::Done(MuxResult::Continue);` from that arm,
12529 /// leaving the `if pass.stalled {}` with an empty body so control falls
12530 /// through to `self.stream_table.set_expect_write(None);`. Measured: this test fails
12531 /// on its own first assertion, `a stalled resume must leave expect_write
12532 /// parked`, with `left: None, right: Some(Other { id: 1, gid: 0 })`, and
12533 /// it is the ONLY failure in the suite. The
12534 /// second assertion moves under the same edit (`left: 2, right: 1`), the
12535 /// extra call being the main loop re-flushing the same stream through the
12536 /// delegated loopback socket once the one-entry script is spent.
12537 #[test]
12538 fn a_stalled_resume_does_not_begin_a_scheduler_pass() {
12539 let pool = make_pool_for_invariant_16();
12540 let (mut connection, mut context, gid, _peer) = writable_fixture(
12541 &pool,
12542 &[FIRST_BLOCK, SECOND_BLOCK],
12543 &[(0, SocketResult::WouldBlock)],
12544 );
12545 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
12546
12547 let parked = H2StreamId::Other {
12548 id: REGISTERED_STREAM_ID,
12549 gid,
12550 };
12551 connection.core.stream_table.set_expect_write(Some(parked));
12552 assert_prepare_gate_is_shut(&context, gid);
12553
12554 let result = connection.writable(&mut context, EndpointClient(&mut router));
12555
12556 assert_eq!(
12557 connection.core.stream_table.expect_write(),
12558 Some(parked),
12559 "a stalled resume must leave expect_write parked: clearing it \
12560 would hand a socket-blocked stream back to the scheduler"
12561 );
12562 assert_eq!(
12563 connection.socket.vectored_calls, 1,
12564 "a stalled resume must return before the scheduler pass begins, so \
12565 the registered stream must not be flushed a second time"
12566 );
12567 assert!(
12568 !context.streams[gid].back.out.is_empty(),
12569 "the refused blocks must stay queued for the next pass"
12570 );
12571 assert_eq!(
12572 context.streams[gid].metrics.bout, 0,
12573 "a stalled resume delivered nothing"
12574 );
12575 assert!(
12576 matches!(result, MuxResult::Continue),
12577 "a stalled resume continues the session, got {result:?}"
12578 );
12579 }
12580
12581 /// A socket that takes at most `budget` bytes, then answers
12582 /// `WouldBlock`, and keeps every byte it took in `wire`, in order: what
12583 /// the peer would read. Reads are served from `inbound`.
12584 struct PacedSocket {
12585 stream: mio::net::TcpStream,
12586 budget: usize,
12587 wire: Vec<u8>,
12588 inbound: std::collections::VecDeque<u8>,
12589 }
12590
12591 impl PacedSocket {
12592 fn take(&mut self, buf: &[u8]) -> usize {
12593 let taken = self.budget.min(buf.len());
12594 self.wire.extend_from_slice(&buf[..taken]);
12595 self.budget -= taken;
12596 taken
12597 }
12598 }
12599
12600 impl SocketHandler for PacedSocket {
12601 fn socket_read(&mut self, buf: &mut [u8]) -> (usize, SocketResult) {
12602 let size = buf.len().min(self.inbound.len());
12603 for (slot, byte) in buf.iter_mut().zip(self.inbound.drain(..size)) {
12604 *slot = byte;
12605 }
12606 if size < buf.len() {
12607 (size, SocketResult::WouldBlock)
12608 } else {
12609 (size, SocketResult::Continue)
12610 }
12611 }
12612
12613 fn socket_write(&mut self, buf: &[u8]) -> (usize, SocketResult) {
12614 let taken = self.take(buf);
12615 if taken < buf.len() {
12616 (taken, SocketResult::WouldBlock)
12617 } else {
12618 (taken, SocketResult::Continue)
12619 }
12620 }
12621
12622 fn socket_write_vectored(&mut self, bufs: &[IoSlice]) -> (usize, SocketResult) {
12623 let mut total = 0;
12624 for buf in bufs {
12625 let taken = self.take(buf);
12626 total += taken;
12627 if taken < buf.len() {
12628 return (total, SocketResult::WouldBlock);
12629 }
12630 }
12631 (total, SocketResult::Continue)
12632 }
12633
12634 fn socket_ref(&self) -> &mio::net::TcpStream {
12635 &self.stream
12636 }
12637
12638 fn socket_mut(&mut self) -> &mut mio::net::TcpStream {
12639 &mut self.stream
12640 }
12641
12642 fn peer_addr(&self) -> Option<std::net::SocketAddr> {
12643 None
12644 }
12645
12646 fn protocol(&self) -> crate::socket::TransportProtocol {
12647 crate::socket::TransportProtocol::Tcp
12648 }
12649
12650 fn read_error(&self) {}
12651
12652 fn write_error(&self) {}
12653 }
12654
12655 /// One frame as the peer reads it: `(type, flags, stream id, payload)`.
12656 type PeerFrame = (u8, u8, u32, Vec<u8>);
12657
12658 /// Split `wire` into frames the way the peer's parser does, failing on a
12659 /// frame the bytes cannot hold.
12660 fn peer_frames(mut wire: &[u8]) -> Result<Vec<PeerFrame>, String> {
12661 let mut frames = Vec::new();
12662 while !wire.is_empty() {
12663 if wire.len() < 9 {
12664 return Err(format!("{} trailing bytes: {wire:?}", wire.len()));
12665 }
12666 let len = u32::from_be_bytes([0, wire[0], wire[1], wire[2]]) as usize;
12667 let stream_id = u32::from_be_bytes([wire[5], wire[6], wire[7], wire[8]]) & 0x7fff_ffff;
12668 if wire.len() < 9 + len {
12669 return Err(format!(
12670 "frame type {:#x} announces {len} bytes, {} remain",
12671 wire[3],
12672 wire.len() - 9
12673 ));
12674 }
12675 frames.push((wire[3], wire[4], stream_id, wire[9..9 + len].to_vec()));
12676 wire = &wire[9 + len..];
12677 }
12678 Ok(frames)
12679 }
12680
12681 /// The payload of the DATA frame `a_ping_during_a_half_written_frame_*`
12682 /// queues on stream 1, after its header below.
12683 const HALF_WRITTEN_PAYLOAD: &[u8] = &[b'd'; 32];
12684 const HALF_WRITTEN_HEADER: &[u8] = &[0, 0, 32, 0, 0, 0, 0, 0, 1];
12685
12686 /// Park a 41-byte DATA frame on stream 1 after the socket took 20 bytes
12687 /// of it, feed `inbound` (a PING or SETTINGS), and read it. `between`
12688 /// then runs before the socket is unthrottled, and the connection is
12689 /// driven the way `Mux` drives it, `writable()` and `readable()` when
12690 /// the filtered readiness allows them. Returns the connection, whose
12691 /// socket holds every byte the peer would read.
12692 fn ack_after_half_written_frame(
12693 inbound: &[u8],
12694 between: impl FnOnce(
12695 &mut H2Shell<PacedSocket>,
12696 &mut Context<TestListener>,
12697 &mut Router,
12698 GlobalStreamId,
12699 ),
12700 ) -> H2Shell<PacedSocket> {
12701 let (_pool, mut connection, mut context, mut router, gid) = park_half_written_frame();
12702
12703 connection.socket.inbound.extend(inbound);
12704 for _ in 0..8 {
12705 connection.core.readiness.event.insert(Ready::READABLE);
12706 if !connection.core.readiness.filter_interest().is_readable() {
12707 break;
12708 }
12709 connection.readable(&mut context, EndpointClient(&mut router));
12710 }
12711 assert!(
12712 connection.socket.inbound.is_empty() && !connection.core.output.is_empty(),
12713 "premise: the frame was read and its ACK queued"
12714 );
12715
12716 between(&mut connection, &mut context, &mut router, gid);
12717 drive_unthrottled(&mut connection, &mut context, &mut router);
12718 connection
12719 }
12720
12721 /// Drive `connection` the way `Mux` does, `writable()` and `readable()`
12722 /// when the filtered readiness allows them, with the socket unthrottled.
12723 fn drive_unthrottled(
12724 connection: &mut H2Shell<PacedSocket>,
12725 context: &mut Context<TestListener>,
12726 router: &mut Router,
12727 ) {
12728 connection.socket.budget = usize::MAX;
12729 for _ in 0..16 {
12730 connection.core.readiness.event.insert(Ready::WRITABLE);
12731 if connection.core.readiness.filter_interest().is_writable() {
12732 connection.writable(context, EndpointClient(router));
12733 }
12734 connection.core.readiness.event.insert(Ready::READABLE);
12735 if connection.core.readiness.filter_interest().is_readable() {
12736 connection.readable(context, EndpointClient(router));
12737 }
12738 }
12739 }
12740
12741 /// Park a 41-byte DATA frame on stream 1 after the socket took 20 bytes
12742 /// of it, and return the buffer pool, which must outlive the rest, the
12743 /// connection, its context and router, and the stream's global id.
12744 fn park_half_written_frame() -> (
12745 Rc<RefCell<Pool>>,
12746 H2Shell<PacedSocket>,
12747 Context<TestListener>,
12748 Router,
12749 GlobalStreamId,
12750 ) {
12751 park_cut_frames(&[HALF_WRITTEN_HEADER, HALF_WRITTEN_PAYLOAD], 20)
12752 }
12753
12754 /// [`park_half_written_frame`] for any whole frames of stream 1, queued
12755 /// as `blocks`, of which the socket takes `budget` bytes.
12756 fn park_cut_frames(
12757 blocks: &[&'static [u8]],
12758 budget: usize,
12759 ) -> (
12760 Rc<RefCell<Pool>>,
12761 H2Shell<PacedSocket>,
12762 Context<TestListener>,
12763 Router,
12764 GlobalStreamId,
12765 ) {
12766 let pool = make_pool_for_invariant_16();
12767 let (socket, _peer) = connected_socket();
12768 let mut connection = H2Shell::new(
12769 Ulid::generate(),
12770 PacedSocket {
12771 stream: socket,
12772 budget,
12773 wire: Vec::new(),
12774 inbound: std::collections::VecDeque::new(),
12775 },
12776 Position::Server,
12777 &mut PoolBufferSource::new(Rc::downgrade(&pool)),
12778 H2FloodConfig::default(),
12779 H2ConnectionConfig::default(),
12780 Duration::from_secs(30),
12781 None,
12782 Duration::from_secs(30),
12783 Some((H2StreamId::Zero, 9)),
12784 Ready::READABLE | Ready::WRITABLE | Ready::HUP | Ready::ERROR,
12785 )
12786 .expect("a pool with free buffers must yield an H2 connection");
12787 connection.core.state = H2State::Header;
12788 let mut context = test_context(&pool);
12789 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
12790 let gid = context
12791 .create_stream(Ulid::generate(), 1 << 16)
12792 .expect("test context must create a stream");
12793 connection
12794 .core
12795 .stream_table
12796 .register(REGISTERED_STREAM_ID, gid, connection.core.now);
12797 for &block in blocks {
12798 context.streams[gid]
12799 .back
12800 .out
12801 .push_back(kawa::OutBlock::Store(kawa::Store::Static(block)));
12802 }
12803 assert_prepare_gate_is_shut(&context, gid);
12804
12805 connection.core.readiness.event.insert(Ready::WRITABLE);
12806 connection.writable(&mut context, EndpointClient(&mut router));
12807 assert_eq!(
12808 (
12809 connection.socket.wire.len(),
12810 connection.core.stream_table.expect_write()
12811 ),
12812 (
12813 budget,
12814 Some(H2StreamId::Other {
12815 id: REGISTERED_STREAM_ID,
12816 gid
12817 })
12818 ),
12819 "premise: the frames are cut and their stream parked"
12820 );
12821 (pool, connection, context, router, gid)
12822 }
12823
12824 const HALF_WRITTEN_PING: &[u8] = b"\x00\x00\x08\x06\x00\x00\x00\x00\x00pingpong";
12825 const SECOND_PING: &[u8] = b"\x00\x00\x08\x06\x00\x00\x00\x00\x00pingpon2";
12826
12827 /// `(type, flags, stream id)` of every frame the peer reads, or the
12828 /// parse failure.
12829 fn peer_frame_kinds(wire: &[u8]) -> Result<Vec<(u8, u8, u32)>, String> {
12830 peer_frames(wire).map(|frames| {
12831 frames
12832 .into_iter()
12833 .map(|(kind, flags, stream_id, _)| (kind, flags, stream_id))
12834 .collect()
12835 })
12836 }
12837
12838 /// A PING that arrives while a stream frame is half-written must be
12839 /// acknowledged AFTER that frame, never inside it (#1600).
12840 ///
12841 /// The socket takes 20 bytes of a 41-byte DATA frame, so the stream is
12842 /// parked in `expect_write` and the 21-byte rest of its frame is adopted
12843 /// by the ordered output queue (#1604). The peer then sends a PING. Its
12844 /// ACK is queued behind that rest, so the peer reads the whole DATA
12845 /// frame, then the ACK, and never 17 ACK bytes in the middle of the
12846 /// frame.
12847 ///
12848 /// TO SEE THIS RED: in `ConnectionH2::handle_write`, replace
12849 /// `self.output.adopt_tail(kawa, tail);` by `let _ = (kawa, tail);`, so
12850 /// the rest of the cut frame stays with its stream. Every test on this
12851 /// fixture then fails: this one and the other #1601 tests on
12852 /// `frame_tail`'s `a frame runs past the gathered bytes` debug assertion
12853 /// (the stream's `kawa.out` no longer starts on a frame boundary), the
12854 /// three #1604 tests below on their wire assertion. Verified 2026-09-27.
12855 /// Before #1604 the equivalent mutation was in `queue_zero_output` and
12856 /// failed with `left: Err("frame type 0x64 announces 6579300 bytes, 8
12857 /// remain")` (verified 2026-09-26).
12858 #[test]
12859 fn a_ping_during_a_half_written_frame_is_acknowledged_after_it() {
12860 let connection = ack_after_half_written_frame(HALF_WRITTEN_PING, |_, _, _, _| {});
12861
12862 assert_eq!(
12863 peer_frames(&connection.socket.wire),
12864 Ok(vec![
12865 (0, 0, REGISTERED_STREAM_ID, HALF_WRITTEN_PAYLOAD.to_vec()),
12866 (6, parser::FLAG_ACK, 0, b"pingpong".to_vec()),
12867 ]),
12868 "the peer must read the whole DATA frame, then the PING ACK"
12869 );
12870 assert!(
12871 connection.core.readiness.interest.is_readable(),
12872 "once the ACK is out the connection must read again"
12873 );
12874 }
12875
12876 /// The SETTINGS ACK takes the same path as the PING ACK above (#1600).
12877 #[test]
12878 fn a_settings_during_a_half_written_frame_is_acknowledged_after_it() {
12879 let connection =
12880 ack_after_half_written_frame(b"\x00\x00\x00\x04\x00\x00\x00\x00\x00", |_, _, _, _| {});
12881
12882 assert_eq!(
12883 peer_frames(&connection.socket.wire),
12884 Ok(vec![
12885 (0, 0, REGISTERED_STREAM_ID, HALF_WRITTEN_PAYLOAD.to_vec()),
12886 (4, parser::FLAG_ACK, 0, Vec::new()),
12887 ]),
12888 "the peer must read the whole DATA frame, then the SETTINGS ACK"
12889 );
12890 assert!(
12891 connection.core.readiness.interest.is_readable(),
12892 "once the ACK is out the connection must read again"
12893 );
12894 }
12895
12896 /// An ACK queued behind a half-written frame whose stream is removed
12897 /// before the frame resumes still leaves, and the connection keeps
12898 /// reading: the removal takes nothing that was queued. What the peer
12899 /// reads is asserted by
12900 /// `a_removed_half_written_stream_still_completes_its_frame` (#1604).
12901 ///
12902 /// TO SEE THIS RED: in `ConnectionH2::flush_pending_control_frames`,
12903 /// answer `H2ControlFlushTarget::Proceed` unconditionally at its end. The
12904 /// test then fails with `an ACK behind a removed stream's frame must not
12905 /// be stranded`.
12906 #[test]
12907 fn a_deferred_ack_does_not_strand_reads_when_its_parked_stream_is_removed() {
12908 let connection =
12909 ack_after_half_written_frame(HALF_WRITTEN_PING, |connection, _, _, gid| {
12910 connection
12911 .core
12912 .remove_dead_stream(REGISTERED_STREAM_ID, gid);
12913 });
12914
12915 assert!(
12916 connection.core.output.is_empty(),
12917 "an ACK behind a removed stream's frame must not be stranded"
12918 );
12919 assert!(
12920 connection.core.readiness.interest.is_readable(),
12921 "once the output is flushed the connection must read again"
12922 );
12923 }
12924
12925 /// Two PINGs read while one frame stays parked are both acknowledged,
12926 /// in order, after that frame.
12927 #[test]
12928 fn two_pings_during_one_park_are_acknowledged_after_the_frame() {
12929 let connection = ack_after_half_written_frame(HALF_WRITTEN_PING, |connection, _, _, _| {
12930 connection.socket.inbound.extend(SECOND_PING);
12931 });
12932
12933 assert_eq!(
12934 peer_frames(&connection.socket.wire),
12935 Ok(vec![
12936 (0, 0, REGISTERED_STREAM_ID, HALF_WRITTEN_PAYLOAD.to_vec()),
12937 (6, parser::FLAG_ACK, 0, b"pingpong".to_vec()),
12938 (6, parser::FLAG_ACK, 0, b"pingpon2".to_vec()),
12939 ]),
12940 "both PING ACKs must follow the whole DATA frame, in order"
12941 );
12942 }
12943
12944 /// A SETTINGS then a PING during one park: both ACKs, in order.
12945 #[test]
12946 fn settings_then_ping_during_one_park_are_acknowledged_after_the_frame() {
12947 let connection = ack_after_half_written_frame(
12948 b"\x00\x00\x00\x04\x00\x00\x00\x00\x00",
12949 |connection, _, _, _| {
12950 connection.socket.inbound.extend(SECOND_PING);
12951 },
12952 );
12953
12954 assert_eq!(
12955 peer_frames(&connection.socket.wire),
12956 Ok(vec![
12957 (0, 0, REGISTERED_STREAM_ID, HALF_WRITTEN_PAYLOAD.to_vec()),
12958 (4, parser::FLAG_ACK, 0, Vec::new()),
12959 (6, parser::FLAG_ACK, 0, b"pingpon2".to_vec()),
12960 ]),
12961 "the SETTINGS ACK then the PING ACK must follow the whole DATA frame"
12962 );
12963 }
12964
12965 /// The shutdown drive (`Mux::shutting_down`) calls `flush_output_buffer`,
12966 /// then forces READABLE and calls `readable()`. That read must not eat
12967 /// the queued ACK. Since #1604 it cannot by construction: the read lands
12968 /// in `zero`, which is input only, and the ACK waits in the output queue
12969 /// behind the rest of the half-written frame.
12970 ///
12971 /// TO SEE THIS RED: the `adopt_tail` mutation described on
12972 /// `a_ping_during_a_half_written_frame_is_acknowledged_after_it` fails it
12973 /// on `a frame runs past the gathered bytes`. Verified 2026-09-27. Before
12974 /// #1604, deleting the `zero_holds_output()` block of `poll_read_target`
12975 /// failed it on `no output may be pending in zero when a frame header is
12976 /// read` (verified 2026-09-27).
12977 #[test]
12978 fn a_forced_shutdown_read_does_not_eat_a_deferred_ack() {
12979 let connection =
12980 ack_after_half_written_frame(HALF_WRITTEN_PING, |connection, context, router, _| {
12981 connection.socket.inbound.extend(SECOND_PING);
12982 connection.flush_output_buffer();
12983 connection.core.readiness.interest.insert(Ready::READABLE);
12984 connection.core.readiness.event.insert(Ready::READABLE);
12985 connection.readable(context, EndpointClient(router));
12986 });
12987
12988 assert_eq!(
12989 peer_frames(&connection.socket.wire),
12990 Ok(vec![
12991 (0, 0, REGISTERED_STREAM_ID, HALF_WRITTEN_PAYLOAD.to_vec()),
12992 (6, parser::FLAG_ACK, 0, b"pingpong".to_vec()),
12993 (6, parser::FLAG_ACK, 0, b"pingpon2".to_vec()),
12994 ]),
12995 "the forced shutdown read must not eat the deferred ACK"
12996 );
12997 }
12998
12999 /// A final GOAWAY while a frame is parked and an ACK queued is sent after
13000 /// both: `goaway()` appends to the ordered output queue, behind the rest
13001 /// of the frame and the ACK.
13002 ///
13003 /// TO SEE THIS RED: in `ConnectionH2::goaway`, call `self.output.clear()`
13004 /// before queueing the GOAWAY. The test then fails with `the whole DATA
13005 /// frame, the PING ACK, then the GOAWAY`. Verified 2026-09-27.
13006 #[test]
13007 fn a_goaway_during_a_half_written_frame_follows_it_and_the_ack() {
13008 let connection =
13009 ack_after_half_written_frame(HALF_WRITTEN_PING, |connection, context, _, gid| {
13010 // A stream being answered is linked; an idle one would be
13011 // pruned by the closing connection before its frame resumes.
13012 context.streams[gid].state = StreamState::Linked(mio::Token(1));
13013 let _ = connection.core.goaway(H2Error::NoError);
13014 });
13015
13016 assert_eq!(
13017 peer_frame_kinds(&connection.socket.wire),
13018 Ok(vec![
13019 (0, 0, REGISTERED_STREAM_ID),
13020 (6, parser::FLAG_ACK, 0),
13021 (7, 0, 0)
13022 ]),
13023 "the whole DATA frame, the PING ACK, then the GOAWAY"
13024 );
13025 }
13026
13027 /// The initial graceful GOAWAY (`graceful_goaway` -> `send_initial_goaway`,
13028 /// the call `Mux::shutting_down` makes) behaves like the final one.
13029 ///
13030 /// TO SEE THIS RED: in `ConnectionH2::send_initial_goaway`, call
13031 /// `self.output.clear()` before queueing the GOAWAY. The test then fails
13032 /// with `the whole DATA frame, the PING ACK, then the initial GOAWAY`.
13033 /// Verified 2026-09-27.
13034 #[test]
13035 fn an_initial_goaway_during_a_half_written_frame_follows_it_and_the_ack() {
13036 let connection = ack_after_half_written_frame(HALF_WRITTEN_PING, |connection, _, _, _| {
13037 let now = connection.core.now;
13038 let _ = connection.core.graceful_goaway(now);
13039 });
13040
13041 assert_eq!(
13042 peer_frame_kinds(&connection.socket.wire),
13043 Ok(vec![
13044 (0, 0, REGISTERED_STREAM_ID),
13045 (6, parser::FLAG_ACK, 0),
13046 (7, 0, 0)
13047 ]),
13048 "the whole DATA frame, the PING ACK, then the initial GOAWAY"
13049 );
13050 }
13051
13052 /// A peer RST_STREAM for stream 1, whose DATA frame the fixture parks.
13053 const RST_STREAM_1: &[u8] = b"\x00\x00\x04\x03\x00\x00\x00\x00\x01\x00\x00\x00\x08";
13054
13055 /// A stream parked half-way through a frame and then removed must not
13056 /// leave that frame truncated on the wire (#1604): the bytes written
13057 /// after the removal would land inside what the peer still parses as
13058 /// that frame. Here the removal is `remove_dead_stream` itself, the
13059 /// chokepoint of every removal path, and the next frame is a PING ACK
13060 /// read after the removal.
13061 ///
13062 /// TO SEE THIS RED: on the pre-image, `a0785505` (stream frames written
13063 /// from `kawa.out` and serialised only through `expect_write`), this test
13064 /// and its two siblings below fail on their wire assertion with, in
13065 /// order, `left: Err("frame type 0x0 announces 65536 bytes, 4 remain")`,
13066 /// `... 0 remain")` and `left: Err("frame type 0x0 announces 32 bytes,
13067 /// 28 remain")`: the peer reads the next frame's bytes as the rest of the
13068 /// truncated one. On this tree, the `adopt_tail` mutation described on
13069 /// `a_ping_during_a_half_written_frame_is_acknowledged_after_it` fails
13070 /// all three with the same messages. Verified 2026-09-27.
13071 #[test]
13072 fn a_removed_half_written_stream_still_completes_its_frame() {
13073 let connection =
13074 ack_after_half_written_frame(HALF_WRITTEN_PING, |connection, _, _, gid| {
13075 connection
13076 .core
13077 .remove_dead_stream(REGISTERED_STREAM_ID, gid);
13078 connection.socket.inbound.extend(SECOND_PING);
13079 });
13080
13081 assert_eq!(
13082 peer_frames(&connection.socket.wire),
13083 Ok(vec![
13084 (0, 0, REGISTERED_STREAM_ID, HALF_WRITTEN_PAYLOAD.to_vec()),
13085 (6, parser::FLAG_ACK, 0, b"pingpong".to_vec()),
13086 (6, parser::FLAG_ACK, 0, b"pingpon2".to_vec()),
13087 ]),
13088 "the removed stream's frame must be completed before the ACKs"
13089 );
13090 assert!(
13091 connection.core.readiness.interest.is_readable(),
13092 "once the output is flushed the connection must read again"
13093 );
13094 }
13095
13096 /// The same removal followed by another stream's frame (#1604): the
13097 /// next stream's DATA frame must follow the whole parked frame.
13098 #[test]
13099 fn a_removed_half_written_stream_does_not_corrupt_the_next_stream_frame() {
13100 const NEXT_STREAM_ID: StreamId = 3;
13101 const NEXT_HEADER: &[u8] = &[0, 0, 4, 0, 1, 0, 0, 0, 3];
13102 const NEXT_PAYLOAD: &[u8] = b"next";
13103 let connection =
13104 ack_after_half_written_frame(HALF_WRITTEN_PING, |connection, context, _, gid| {
13105 connection
13106 .core
13107 .remove_dead_stream(REGISTERED_STREAM_ID, gid);
13108 let next = context
13109 .create_stream(Ulid::generate(), 1 << 16)
13110 .expect("test context must create a second stream");
13111 connection
13112 .core
13113 .stream_table
13114 .register(NEXT_STREAM_ID, next, connection.core.now);
13115 for block in [NEXT_HEADER, NEXT_PAYLOAD] {
13116 context.streams[next]
13117 .back
13118 .out
13119 .push_back(kawa::OutBlock::Store(kawa::Store::Static(block)));
13120 }
13121 });
13122
13123 assert_eq!(
13124 peer_frames(&connection.socket.wire),
13125 Ok(vec![
13126 (0, 0, REGISTERED_STREAM_ID, HALF_WRITTEN_PAYLOAD.to_vec()),
13127 (6, parser::FLAG_ACK, 0, b"pingpong".to_vec()),
13128 (0, 1, NEXT_STREAM_ID, NEXT_PAYLOAD.to_vec()),
13129 ]),
13130 "the next stream's frame must follow the whole parked frame"
13131 );
13132 }
13133
13134 /// HEADERS without END_HEADERS, then its CONTINUATION, on stream 1.
13135 const CUT_BLOCK_HEADERS: &[u8] = b"\x00\x00\x10\x01\x00\x00\x00\x00\x01hpack-fragment-1";
13136 const CUT_BLOCK_CONTINUATION: &[u8] = b"\x00\x00\x10\x09\x04\x00\x00\x00\x01hpack-fragment-2";
13137
13138 /// A header block cut by a partial write keeps everything else out until
13139 /// END_HEADERS (RFC 9113 §6.10): the PING ACK read meanwhile follows the
13140 /// CONTINUATION, whether the write stopped inside the HEADERS frame or
13141 /// exactly on the boundary between the two frames, where no frame is
13142 /// half-written but the block still is (#1604).
13143 ///
13144 /// TO SEE THIS RED: in `h2_transmit::frame_tail`, append `&& false` to
13145 /// the `in_block` expression, so the rest stops at the cut frame's end.
13146 /// The test then fails with `the ACK must follow END_HEADERS`, the ACK
13147 /// sitting between the HEADERS and the CONTINUATION. Verified
13148 /// 2026-09-27.
13149 #[test]
13150 fn a_cut_header_block_is_completed_before_the_ack() {
13151 for budget in [20, CUT_BLOCK_HEADERS.len()] {
13152 let (_pool, mut connection, mut context, mut router, _gid) =
13153 park_cut_frames(&[CUT_BLOCK_HEADERS, CUT_BLOCK_CONTINUATION], budget);
13154 connection.socket.inbound.extend(HALF_WRITTEN_PING);
13155 for _ in 0..4 {
13156 connection.core.readiness.event.insert(Ready::READABLE);
13157 connection.readable(&mut context, EndpointClient(&mut router));
13158 }
13159 assert!(
13160 connection.socket.inbound.is_empty(),
13161 "premise: the PING was read while the block is cut"
13162 );
13163 drive_unthrottled(&mut connection, &mut context, &mut router);
13164
13165 assert_eq!(
13166 peer_frame_kinds(&connection.socket.wire),
13167 Ok(vec![
13168 (1, 0, REGISTERED_STREAM_ID),
13169 (9, parser::FLAG_END_HEADERS, REGISTERED_STREAM_ID),
13170 (6, parser::FLAG_ACK, 0),
13171 ]),
13172 "the ACK must follow END_HEADERS (cut at {budget})"
13173 );
13174 }
13175 }
13176
13177 /// The peer resets the parked stream (#1604). Its RST_STREAM is read
13178 /// while the frame is half-written, and retires the stream; the rest of
13179 /// that frame must still reach the wire before the PING ACK behind it.
13180 #[test]
13181 fn a_peer_reset_of_a_half_written_stream_still_completes_its_frame() {
13182 let (_pool, mut connection, mut context, mut router, _gid) = park_half_written_frame();
13183 connection.socket.inbound.extend(RST_STREAM_1);
13184 connection.core.readiness.event.insert(Ready::READABLE);
13185 connection.readable(&mut context, EndpointClient(&mut router));
13186 connection.core.readiness.event.insert(Ready::READABLE);
13187 connection.readable(&mut context, EndpointClient(&mut router));
13188 assert!(
13189 connection.socket.inbound.is_empty()
13190 && !connection
13191 .core
13192 .stream_table
13193 .streams()
13194 .contains_key(®ISTERED_STREAM_ID),
13195 "premise: the RST was read and retired the parked stream"
13196 );
13197
13198 connection.socket.inbound.extend(HALF_WRITTEN_PING);
13199 drive_unthrottled(&mut connection, &mut context, &mut router);
13200
13201 assert_eq!(
13202 peer_frames(&connection.socket.wire),
13203 Ok(vec![
13204 (0, 0, REGISTERED_STREAM_ID, HALF_WRITTEN_PAYLOAD.to_vec()),
13205 (6, parser::FLAG_ACK, 0, b"pingpong".to_vec()),
13206 ]),
13207 "the reset stream's frame must be completed before the ACK"
13208 );
13209 }
13210
13211 /// Park stream 1's encoded 404 unsent behind a socket that takes nothing,
13212 /// let `drop_block` discard it, then drive the connection unthrottled and
13213 /// decode every HEADERS the peer reads with ONE decoder, as the peer does.
13214 /// Answers `(stream id, decoded)` per HEADERS and the `:status` values read.
13215 ///
13216 /// Both requests get the same 404, so stream 1's block inserts the
13217 /// answer's fields into the connection's encoder table and stream 3's
13218 /// identical block names them by index: a peer that missed stream 1's
13219 /// block cannot read stream 3's without a table reset.
13220 fn peer_reads_after_a_dropped_parked_block(
13221 drop_block: impl FnOnce(&mut H2Shell<PacedSocket>, &mut Context<TestListener>, &mut Router),
13222 ) -> (Vec<(u32, bool)>, Vec<Vec<u8>>) {
13223 let (_pool, mut connection, mut context, mut router, _peer) = two_requests_read(0);
13224 let answers = context.listener.borrow().get_answers().clone();
13225 let open: Vec<GlobalStreamId> = connection
13226 .core
13227 .stream_table
13228 .streams()
13229 .values()
13230 .copied()
13231 .collect();
13232 for gid in open {
13233 crate::protocol::mux::answers::set_default_answer(
13234 &mut context.streams[gid],
13235 &mut connection.core.readiness,
13236 404,
13237 &answers.borrow(),
13238 );
13239 }
13240 connection.core.readiness.event.insert(Ready::WRITABLE);
13241 connection.writable(&mut context, EndpointClient(&mut router));
13242 assert!(
13243 connection.socket.wire.is_empty()
13244 && connection.core.stream_table.expect_write().is_some(),
13245 "premise: stream 1's block was encoded and parked unsent"
13246 );
13247 drop_block(&mut connection, &mut context, &mut router);
13248 connection.socket.budget = usize::MAX;
13249 drive_both_ways(&mut connection, &mut context, &mut router);
13250 peer_header_blocks(&connection.socket.wire)
13251 }
13252
13253 /// An H2 server connection over a [`PacedSocket`] taking `budget` bytes,
13254 /// with GET requests on streams 1 and 3 read and not answered.
13255 fn two_requests_read(
13256 budget: usize,
13257 ) -> (
13258 Rc<RefCell<Pool>>,
13259 H2Shell<PacedSocket>,
13260 Context<TestListener>,
13261 Router,
13262 std::net::TcpStream,
13263 ) {
13264 let pool = make_pool_for_invariant_16();
13265 let (socket, peer) = connected_socket();
13266 let mut connection = H2Shell::new(
13267 Ulid::generate(),
13268 PacedSocket {
13269 stream: socket,
13270 budget,
13271 wire: Vec::new(),
13272 inbound: std::collections::VecDeque::new(),
13273 },
13274 Position::Server,
13275 &mut PoolBufferSource::new(Rc::downgrade(&pool)),
13276 H2FloodConfig::default(),
13277 H2ConnectionConfig::default(),
13278 Duration::from_secs(30),
13279 None,
13280 Duration::from_secs(30),
13281 Some((H2StreamId::Zero, 9)),
13282 Ready::READABLE | Ready::WRITABLE | Ready::HUP | Ready::ERROR,
13283 )
13284 .expect("pool");
13285 connection.core.state = H2State::Header;
13286 let mut context = test_context(&pool);
13287 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
13288 let mut peer_encoder = crate::protocol::mux::hpack::Encoder::new();
13289 for stream_id in [1u32, 3] {
13290 let block = peer_encoder.encode([
13291 (&b":method"[..], &b"GET"[..]),
13292 (&b":scheme"[..], &b"https"[..]),
13293 (&b":authority"[..], &b"example.com"[..]),
13294 (&b":path"[..], &b"/repro"[..]),
13295 ]);
13296 let mut frame = (block.len() as u32).to_be_bytes()[1..].to_vec();
13297 frame.push(1);
13298 frame.push(parser::FLAG_END_STREAM | parser::FLAG_END_HEADERS);
13299 frame.extend_from_slice(&stream_id.to_be_bytes());
13300 frame.extend_from_slice(&block);
13301 connection.socket.inbound.extend(frame);
13302 }
13303 for _ in 0..8 {
13304 connection.core.readiness.event.insert(Ready::READABLE);
13305 connection.readable(&mut context, EndpointClient(&mut router));
13306 }
13307 assert_eq!(
13308 connection.core.stream_table.len(),
13309 2,
13310 "premise: both requests were read"
13311 );
13312 (pool, connection, context, router, peer)
13313 }
13314
13315 /// Drive the connection the way `Mux` does, `writable()` and
13316 /// `readable()` when the filtered readiness allows them.
13317 fn drive_both_ways(
13318 connection: &mut H2Shell<PacedSocket>,
13319 context: &mut Context<TestListener>,
13320 router: &mut Router,
13321 ) {
13322 for _ in 0..16 {
13323 connection.core.readiness.event.insert(Ready::WRITABLE);
13324 if connection.core.readiness.filter_interest().is_writable() {
13325 connection.writable(context, EndpointClient(router));
13326 }
13327 connection.core.readiness.event.insert(Ready::READABLE);
13328 if connection.core.readiness.filter_interest().is_readable() {
13329 connection.readable(context, EndpointClient(router));
13330 }
13331 }
13332 }
13333
13334 /// Decode every HEADERS on `wire` with ONE decoder, as the peer does.
13335 /// Answers `(stream id, decoded)` per HEADERS and the `:status` values read.
13336 fn peer_header_blocks(wire: &[u8]) -> (Vec<(u32, bool)>, Vec<Vec<u8>>) {
13337 let mut decoder = crate::protocol::mux::hpack::Decoder::new();
13338 let mut seen = Vec::new();
13339 let mut status = Vec::new();
13340 for (kind, _, stream_id, payload) in peer_frames(wire).unwrap() {
13341 if kind == 1 {
13342 let decoded = decoder.decode_with_cb(&payload, |name, value| {
13343 if &*name == b":status" {
13344 status.push(value.into_owned());
13345 }
13346 });
13347 seen.push((stream_id, decoded.is_ok()));
13348 }
13349 }
13350 (seen, status)
13351 }
13352
13353 /// A header block the encoder produced but the wire never started is
13354 /// dropped with its stream, and the peer's HPACK table must not fall
13355 /// behind for it (sozu-proxy/sozu#1627, path 1): the peer's RST_STREAM(1)
13356 /// removes the parked stream 1, and stream 3's block must still decode.
13357 ///
13358 /// TO SEE THIS RED: in `ConnectionH2::poll_write_target`'s
13359 /// `H2WritePhase::Start` arm, delete the `self.reset_encoder_table()` call.
13360 /// The peer then fails to decode stream 3's block: `left: [(3, false)],
13361 /// right: [(3, true)]`. Verified 2026-09-27 (red on `58550dd4`).
13362 #[test]
13363 fn a_header_block_dropped_with_its_stream_keeps_the_peer_table_in_sync() {
13364 let (seen, status) =
13365 peer_reads_after_a_dropped_parked_block(|connection, context, router| {
13366 connection.socket.inbound.extend(RST_STREAM_1);
13367 for _ in 0..4 {
13368 connection.core.readiness.event.insert(Ready::READABLE);
13369 connection.readable(context, EndpointClient(router));
13370 }
13371 assert!(
13372 !connection.core.stream_table.streams().contains_key(&1),
13373 "premise: the RST removed stream 1"
13374 );
13375 });
13376 assert_eq!(
13377 seen,
13378 vec![(3, true)],
13379 "the peer must decode stream 3's block with its own table"
13380 );
13381 assert_eq!(status, vec![b"404".to_vec()], "and read what was encoded");
13382 }
13383
13384 /// The park outlives a second write pass that is stalled too: that pass
13385 /// resumes the parked stream, writes nothing, and parks it again, so it
13386 /// must record again that the park holds an encoded block. The peer's
13387 /// RST_STREAM(1) then removes it, and stream 3's block must still decode
13388 /// (sozu-proxy/sozu#1627).
13389 ///
13390 /// TO SEE THIS RED: in `ConnectionH2::poll_write_target`'s
13391 /// `H2WritePhase::Resume` arm, delete the `self.parked_header_block = ..`
13392 /// assignment before the stalled `return`. The peer then fails on stream
13393 /// 3's block: `left: [(3, false)], right: [(3, true)]`. Verified
13394 /// 2026-09-27.
13395 #[test]
13396 fn a_header_block_parked_across_two_stalled_passes_keeps_the_peer_table_in_sync() {
13397 let (seen, status) =
13398 peer_reads_after_a_dropped_parked_block(|connection, context, router| {
13399 connection.core.readiness.event.insert(Ready::WRITABLE);
13400 connection.writable(context, EndpointClient(router));
13401 assert!(
13402 connection.socket.wire.is_empty()
13403 && connection.core.stream_table.expect_write().is_some(),
13404 "premise: the second pass resumed nothing and kept the park"
13405 );
13406 connection.socket.inbound.extend(RST_STREAM_1);
13407 for _ in 0..4 {
13408 connection.core.readiness.event.insert(Ready::READABLE);
13409 connection.readable(context, EndpointClient(router));
13410 }
13411 assert!(
13412 !connection.core.stream_table.streams().contains_key(&1),
13413 "premise: the RST removed stream 1"
13414 );
13415 });
13416 assert_eq!(
13417 seen,
13418 vec![(3, true)],
13419 "the peer must decode stream 3's block with its own table"
13420 );
13421 assert_eq!(status, vec![b"404".to_vec()], "and read what was encoded");
13422 }
13423
13424 /// An H2 backend connection (`Position::Client`, `Connected`, handshake
13425 /// done) over a [`PacedSocket`] taking `budget` bytes.
13426 fn paced_backend(pool: &Rc<RefCell<Pool>>, budget: usize) -> H2Shell<PacedSocket> {
13427 let (socket, _peer) = connected_socket();
13428 let backend = Rc::new(RefCell::new(crate::backends::Backend::new(
13429 "relink-backend",
13430 "127.0.0.1:2".parse().expect("backend address must parse"),
13431 None,
13432 None,
13433 None,
13434 )));
13435 let mut registry = crate::protocol::mux::BackendRegistry::default();
13436 let mut connection = H2Shell::new(
13437 Ulid::generate(),
13438 PacedSocket {
13439 stream: socket,
13440 budget,
13441 wire: Vec::new(),
13442 inbound: std::collections::VecDeque::new(),
13443 },
13444 Position::Client(
13445 "relink-cluster".into(),
13446 registry.id_for(&backend),
13447 BackendStatus::Connected,
13448 ),
13449 &mut PoolBufferSource::new(Rc::downgrade(pool)),
13450 H2FloodConfig::default(),
13451 H2ConnectionConfig::default(),
13452 Duration::from_secs(30),
13453 None,
13454 Duration::from_secs(30),
13455 Some((H2StreamId::Zero, 9)),
13456 Ready::READABLE | Ready::WRITABLE | Ready::HUP | Ready::ERROR,
13457 )
13458 .expect("a pool with free buffers must yield an H2 connection");
13459 connection.core.state = H2State::Header;
13460 connection
13461 }
13462
13463 /// Queue a GET request carrying `x-api-key: <key>` on `gid`'s request
13464 /// side, closed with END_STREAM, the way a relayed request reaches a
13465 /// backend's write pass. The custom field is indexed into the encoder's
13466 /// dynamic table, as a credential-like header would be.
13467 fn queue_backend_request(
13468 context: &mut Context<TestListener>,
13469 gid: GlobalStreamId,
13470 key: &'static [u8],
13471 ) {
13472 let kawa = &mut context.streams[gid].front;
13473 kawa.detached.status_line = kawa::StatusLine::Request {
13474 version: kawa::Version::V20,
13475 method: kawa::Store::Static(b"GET"),
13476 uri: kawa::Store::Static(b"/"),
13477 authority: kawa::Store::Static(b"example.com"),
13478 path: kawa::Store::Static(b"/"),
13479 };
13480 kawa.push_block(kawa::Block::StatusLine);
13481 kawa.push_block(kawa::Block::Header(kawa::Pair {
13482 key: kawa::Store::Static(b"x-api-key"),
13483 val: kawa::Store::Static(key),
13484 }));
13485 kawa.push_block(kawa::Block::Flags(kawa::Flags {
13486 end_body: false,
13487 end_chunk: false,
13488 end_header: true,
13489 end_stream: true,
13490 }));
13491 kawa.parsing_phase = kawa::ParsingPhase::Terminated;
13492 }
13493
13494 /// The #1632 starting point: a request linked to backend `backend`,
13495 /// whose write pass HPACK-encoded its HEADERS while the socket took
13496 /// nothing. Answers the request's global id and the exact bytes that
13497 /// connection encoded for it.
13498 fn park_an_encoded_request(
13499 backend: &mut H2Shell<PacedSocket>,
13500 context: &mut Context<TestListener>,
13501 router: &mut Router,
13502 ) -> (GlobalStreamId, Vec<u8>) {
13503 let gid = context
13504 .create_stream(Ulid::generate(), 1 << 16)
13505 .expect("test context must create a stream");
13506 queue_backend_request(context, gid, b"alice-secret");
13507 context.streams[gid].state = StreamState::Linked(mio::Token(1));
13508 assert!(backend.start_stream(gid, context), "premise: stream 1");
13509 backend.core.readiness.event.insert(Ready::WRITABLE);
13510 backend.writable(context, EndpointClient(router));
13511 let front = &context.streams[gid].front;
13512 assert!(
13513 backend.socket.wire.is_empty() && !front.out.is_empty() && !front.consumed,
13514 "premise: the request was encoded and parked unsent"
13515 );
13516 let encoded: Vec<u8> = front
13517 .as_io_slice()
13518 .iter()
13519 .flat_map(|slice| slice.iter().copied())
13520 .collect();
13521 (gid, encoded)
13522 }
13523
13524 /// RFC 9113 §6.9: a proxied stream has an independent send window on each
13525 /// connection. The client granted the maximum initial window on the
13526 /// frontend connection; the H2 backend then advertises
13527 /// `SETTINGS_INITIAL_WINDOW_SIZE = 2^31-1` too. Each value is within the
13528 /// §6.9.2 ceiling, so the backend connection must acknowledge it and send
13529 /// the request, not overflow a window it shares with the frontend leg and
13530 /// answer GOAWAY.
13531 ///
13532 /// TO SEE THIS RED: make `Stream::send_window_mut` and `Stream::split`
13533 /// answer `front_window` for `Position::Client` too. The backend then
13534 /// adds its delta to the client's 2^31-1: `the backend connection must
13535 /// not answer a valid initial window with GOAWAY`. Verified 2026-10-01
13536 /// (red on `3d3f077b`).
13537 #[test]
13538 fn a_backend_max_initial_window_is_independent_of_the_frontend_window() {
13539 let pool = make_pool_for_invariant_16();
13540 let mut backend = paced_backend(&pool, usize::MAX);
13541 let mut context = test_context(&pool);
13542 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
13543 // What a frontend connection whose client advertised 2^31-1 creates.
13544 let gid = context
13545 .create_stream(Ulid::generate(), FLOW_CONTROL_MAX_WINDOW)
13546 .expect("test context must create a stream");
13547 queue_backend_request(&mut context, gid, b"key");
13548 context.streams[gid].state = StreamState::Linked(mio::Token(1));
13549 assert!(backend.start_stream(gid, &mut context), "premise: stream 1");
13550
13551 let mut max_initial_window = vec![0, 4];
13552 max_initial_window.extend_from_slice(&FLOW_CONTROL_MAX_WINDOW.to_be_bytes());
13553 backend
13554 .socket
13555 .inbound
13556 .extend(orphan_frame(4, 0, 0, 6, &max_initial_window));
13557 for _ in 0..4 {
13558 backend.core.readiness.event.insert(Ready::READABLE);
13559 if backend.core.readiness.filter_interest().is_readable() {
13560 backend.readable(&mut context, EndpointClient(&mut router));
13561 }
13562 }
13563 backend.core.readiness.event.insert(Ready::WRITABLE);
13564 backend.writable(&mut context, EndpointClient(&mut router));
13565
13566 let frames = peer_frames(&backend.socket.wire).expect("whole frames on the wire");
13567 assert!(
13568 !frames.iter().any(|(kind, ..)| *kind == 7),
13569 "the backend connection must not answer a valid initial window with GOAWAY"
13570 );
13571 assert!(
13572 frames
13573 .iter()
13574 .any(|(kind, flags, ..)| *kind == 4 && *flags == 1),
13575 "the backend's SETTINGS are acknowledged"
13576 );
13577 assert!(
13578 frames.iter().any(|(kind, _, id, _)| *kind == 1 && *id == 1),
13579 "and the request leaves on stream 1"
13580 );
13581 }
13582
13583 /// A 4-octet WINDOW_UPDATE frame for `stream_id`.
13584 fn window_update_frame(stream_id: u32, increment: u32) -> Vec<u8> {
13585 orphan_frame(8, 0, stream_id, 4, &increment.to_be_bytes())
13586 }
13587
13588 /// RFC 9113 §6.9 on the backend leg: `start_stream` sizes the stream's
13589 /// send window from the initial window the BACKEND advertised (here
13590 /// 1000, already received on a pooled connection), whatever the frontend
13591 /// granted: larger (an H2 client at 2^31-1), the `1 << 16` an H1
13592 /// frontend seeds, or nothing. Taking the frontend's value would let a
13593 /// request body overrun the backend's window. The backend's WINDOW_UPDATE
13594 /// credits that leg only.
13595 ///
13596 /// TO SEE THIS RED: delete the `send_window_mut` assignment in
13597 /// `ConnectionH2::start_stream`: `the backend leg starts at the
13598 /// backend's window`, `left: 65535, right: 1000`. Verified 2026-10-01.
13599 #[test]
13600 fn a_backend_stream_window_is_sized_and_credited_by_the_backend_only() {
13601 const BACKEND_WINDOW: u32 = 1000;
13602 for front_window in [FLOW_CONTROL_MAX_WINDOW, 1 << 16, 0] {
13603 let pool = make_pool_for_invariant_16();
13604 let mut backend = paced_backend(&pool, usize::MAX);
13605 let mut context = test_context(&pool);
13606 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
13607 let read = |backend: &mut H2Shell<PacedSocket>,
13608 context: &mut Context<TestListener>,
13609 router: &mut Router| {
13610 for _ in 0..4 {
13611 backend.core.readiness.event.insert(Ready::READABLE);
13612 if backend.core.readiness.filter_interest().is_readable() {
13613 backend.readable(context, EndpointClient(router));
13614 }
13615 }
13616 };
13617 let mut initial_window = vec![0, 4];
13618 initial_window.extend_from_slice(&BACKEND_WINDOW.to_be_bytes());
13619 backend
13620 .socket
13621 .inbound
13622 .extend(orphan_frame(4, 0, 0, 6, &initial_window));
13623 read(&mut backend, &mut context, &mut router);
13624 let gid = context
13625 .create_stream(Ulid::generate(), front_window)
13626 .expect("test context must create a stream");
13627 queue_backend_request(&mut context, gid, b"key");
13628 context.streams[gid].state = StreamState::Linked(mio::Token(1));
13629 assert!(backend.start_stream(gid, &mut context), "premise: stream 1");
13630 assert_eq!(
13631 context.streams[gid].back_window, BACKEND_WINDOW as i32,
13632 "front window {front_window}: the backend leg starts at the backend's window"
13633 );
13634
13635 backend.socket.inbound.extend(window_update_frame(1, 500));
13636 read(&mut backend, &mut context, &mut router);
13637 let stream = &context.streams[gid];
13638 assert_eq!(
13639 stream.back_window,
13640 BACKEND_WINDOW as i32 + 500,
13641 "front window {front_window}: the backend's WINDOW_UPDATE credits its leg"
13642 );
13643 assert_eq!(
13644 stream.front_window,
13645 i32::try_from(front_window).unwrap(),
13646 "front window {front_window}: and leaves the frontend leg alone"
13647 );
13648 }
13649 }
13650
13651 /// Feed `inbound` to a frontend connection carrying streams 1 and 3,
13652 /// flush what it answers, and return the GOAWAY error codes it wrote.
13653 fn goaway_codes_after(
13654 connection: &mut H2Shell<PacedSocket>,
13655 context: &mut Context<TestListener>,
13656 router: &mut Router,
13657 inbound: &[u8],
13658 ) -> Vec<u32> {
13659 connection.socket.inbound.extend(inbound);
13660 for _ in 0..4 {
13661 connection.core.readiness.event.insert(Ready::READABLE);
13662 if connection.core.readiness.filter_interest().is_readable() {
13663 connection.readable(context, EndpointClient(router));
13664 }
13665 }
13666 connection.core.readiness.event.insert(Ready::WRITABLE);
13667 connection.writable(context, EndpointClient(router));
13668 peer_frames(&connection.socket.wire)
13669 .expect("whole frames on the wire")
13670 .iter()
13671 .filter(|(kind, ..)| *kind == 7)
13672 .map(|(_, _, _, payload)| {
13673 u32::from_be_bytes(payload[4..8].try_into().expect("GOAWAY error code"))
13674 })
13675 .collect()
13676 }
13677
13678 /// A SETTINGS frame carrying only `SETTINGS_INITIAL_WINDOW_SIZE = value`.
13679 fn initial_window_settings(value: u32) -> Vec<u8> {
13680 let mut payload = vec![0, 4];
13681 payload.extend_from_slice(&value.to_be_bytes());
13682 orphan_frame(4, 0, 0, 6, &payload)
13683 }
13684
13685 /// RFC 9113 §6.5.2: a `SETTINGS_INITIAL_WINDOW_SIZE` above 2^31-1 "MUST
13686 /// be treated as a connection error of type FLOW_CONTROL_ERROR".
13687 ///
13688 /// TO SEE THIS RED: answer the `SETTINGS_INITIAL_WINDOW_SIZE` arm of
13689 /// `ConnectionH2::handle_settings_frame` with `H2Error::ProtocolError`:
13690 /// `left: [1], right: [3]`. Verified 2026-10-01 (red on `c8779209`).
13691 #[test]
13692 fn an_initial_window_above_the_maximum_is_a_flow_control_error() {
13693 let (_pool, mut connection, mut context, mut router, _peer) = two_requests_read(usize::MAX);
13694 let codes = goaway_codes_after(
13695 &mut connection,
13696 &mut context,
13697 &mut router,
13698 &initial_window_settings(FLOW_CONTROL_MAX_WINDOW + 1),
13699 );
13700 assert_eq!(codes, vec![0x3], "GOAWAY(FLOW_CONTROL_ERROR)");
13701 }
13702
13703 /// RFC 9113 §6.9.2: a SETTINGS change that pushes a stream window past
13704 /// 2^31-1 is a connection error of type FLOW_CONTROL_ERROR. Stream 3's
13705 /// window already stands at 2^31-1; raising the initial window by one
13706 /// overflows it. The rejection is checked before any window moves, so
13707 /// stream 1, walked first, keeps its window.
13708 ///
13709 /// TO SEE THIS RED: in `ConnectionH2::update_initial_window_size`,
13710 /// apply each delta as it is checked, returning on the first overflow:
13711 /// `the rejected setting changes no stream window`, `left: 65536, right:
13712 /// 65535`. Verified 2026-10-01 (red on `c8779209`).
13713 #[test]
13714 fn a_settings_change_overflowing_a_stream_window_is_a_flow_control_error() {
13715 let (_pool, mut connection, mut context, mut router, _peer) = two_requests_read(usize::MAX);
13716 let gid = |connection: &H2Shell<PacedSocket>, id: u32| {
13717 *connection
13718 .core
13719 .stream_table
13720 .streams()
13721 .get(&id)
13722 .expect("the stream is open")
13723 };
13724 let (first, third) = (gid(&connection, 1), gid(&connection, 3));
13725 let mut inbound = orphan_frame(
13726 8,
13727 0,
13728 3,
13729 4,
13730 &(FLOW_CONTROL_MAX_WINDOW - DEFAULT_INITIAL_WINDOW_SIZE).to_be_bytes(),
13731 );
13732 inbound.extend(initial_window_settings(DEFAULT_INITIAL_WINDOW_SIZE + 1));
13733 let codes = goaway_codes_after(&mut connection, &mut context, &mut router, &inbound);
13734 assert_eq!(
13735 context.streams[third].front_window, FLOW_CONTROL_MAX_WINDOW as i32,
13736 "premise: stream 3's window stands at 2^31-1"
13737 );
13738 assert_eq!(codes, vec![0x3], "GOAWAY(FLOW_CONTROL_ERROR)");
13739 assert_eq!(
13740 context.streams[first].front_window, DEFAULT_INITIAL_WINDOW_SIZE as i32,
13741 "the rejected setting changes no stream window"
13742 );
13743 }
13744
13745 /// RFC 9113 §6.9 on the frontend leg: the client's WINDOW_UPDATE and
13746 /// SETTINGS_INITIAL_WINDOW_SIZE move the stream's frontend window only.
13747 #[test]
13748 fn a_frontend_window_update_credits_the_frontend_leg_only() {
13749 let (_pool, mut connection, mut context, mut router, _peer) = two_requests_read(usize::MAX);
13750 let first = *connection
13751 .core
13752 .stream_table
13753 .streams()
13754 .get(&1)
13755 .expect("the stream is open");
13756 context.streams[first].back_window = 1234;
13757 let mut initial_window = vec![0, 4];
13758 initial_window.extend_from_slice(&100_000u32.to_be_bytes());
13759 connection
13760 .socket
13761 .inbound
13762 .extend(window_update_frame(1, 1000));
13763 connection
13764 .socket
13765 .inbound
13766 .extend(orphan_frame(4, 0, 0, 6, &initial_window));
13767 for _ in 0..4 {
13768 connection.core.readiness.event.insert(Ready::READABLE);
13769 if connection.core.readiness.filter_interest().is_readable() {
13770 connection.readable(&mut context, EndpointClient(&mut router));
13771 }
13772 }
13773 let stream = &context.streams[first];
13774 assert_eq!(
13775 stream.front_window,
13776 100_000 + 1000,
13777 "the client's WINDOW_UPDATE and initial window move the frontend leg"
13778 );
13779 assert_eq!(stream.back_window, 1234, "and not the backend leg");
13780 }
13781
13782 /// sozu-proxy/sozu#1632, GOAWAY form: the backend refuses the stream
13783 /// (`last_stream_id` below it) after its HEADERS were encoded but not
13784 /// sent. It was not processed, but its request cannot be sent anywhere
13785 /// else: `front.out` holds this connection's frames and the blocks are
13786 /// gone. It must be refused (REFUSED_STREAM toward the client, RFC 9113
13787 /// §8.7), never queued for a re-link.
13788 ///
13789 /// TO SEE THIS RED: in [`ConnectionH2::handle_goaway_frame`], test
13790 /// `stream.front.consumed` alone. The stream is then re-linked with its
13791 /// stale frames: `a request encoded for this backend must not be re-linked
13792 /// after its GOAWAY`. Red on `d5161919`, verified 2026-09-28.
13793 #[test]
13794 fn a_request_encoded_for_a_backend_is_refused_on_goaway_not_relinked() {
13795 let pool = make_pool_for_invariant_16();
13796 let mut backend = paced_backend(&pool, 0);
13797 let mut context = test_context(&pool);
13798 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
13799 let (gid, _) = park_an_encoded_request(&mut backend, &mut context, &mut router);
13800
13801 // GOAWAY(last_stream_id = 0, NO_ERROR): stream 1 was not processed.
13802 backend
13803 .socket
13804 .inbound
13805 .extend(orphan_frame(7, 0, 0, 8, &[0; 8]));
13806 for _ in 0..4 {
13807 backend.core.readiness.event.insert(Ready::READABLE);
13808 if backend.core.readiness.filter_interest().is_readable() {
13809 backend.readable(&mut context, EndpointClient(&mut router));
13810 }
13811 }
13812
13813 assert!(
13814 !context.pending_links.contains(&gid),
13815 "a request encoded for this backend must not be re-linked after its GOAWAY"
13816 );
13817 let stream = &context.streams[gid];
13818 assert!(
13819 stream.context.status == Some(503) && stream.state == StreamState::Unlinked,
13820 "the client must be answered 503, got {:?} / {:?}",
13821 stream.context.status,
13822 stream.state
13823 );
13824 }
13825
13826 /// sozu-proxy/sozu#1632, H1 frontend form (found by the adversarial
13827 /// review of the first fix): behind a GOAWAY that refuses a request its
13828 /// H2 backend encoded but did not send, an H1 client must receive an
13829 /// error response, not silence. The first fix answered with
13830 /// `forcefully_terminate_answer(REFUSED_STREAM)`, an empty response kawa
13831 /// in `Error`, which `ConnectionH1::writable` cannot write: it withdraws
13832 /// WRITABLE and the client waits for its timeout.
13833 ///
13834 /// TO SEE THIS RED: in [`ConnectionH2::handle_goaway_frame`], answer the
13835 /// `front_bound_to_backend` branch with `forcefully_terminate_answer(..,
13836 /// H2Error::RefusedStream)` as the consumed branch does. The H1 client
13837 /// then reads nothing: `the H1 client must read an error response, got
13838 /// ""`. Verified 2026-09-28.
13839 #[test]
13840 fn an_h1_client_behind_a_goaway_refusing_its_encoded_request_gets_a_response() {
13841 use std::io::Read;
13842
13843 let pool = make_pool_for_invariant_16();
13844 let mut backend = paced_backend(&pool, 0);
13845 let mut context = test_context(&pool);
13846 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
13847 let (gid, _) = park_an_encoded_request(&mut backend, &mut context, &mut router);
13848 let (socket, mut client) = connected_socket();
13849 let mut frontend =
13850 Connection::new_h1_server(Ulid::generate(), socket, Duration::from_secs(30));
13851 assert_eq!(gid, 0, "premise: the H1 frontend serves slot 0");
13852
13853 backend
13854 .socket
13855 .inbound
13856 .extend(orphan_frame(7, 0, 0, 8, &[0; 8]));
13857 for _ in 0..4 {
13858 backend.core.readiness.event.insert(Ready::READABLE);
13859 if backend.core.readiness.filter_interest().is_readable() {
13860 backend.readable(
13861 &mut context,
13862 crate::protocol::mux::connection::EndpointServer(&mut frontend),
13863 );
13864 }
13865 }
13866 assert!(
13867 !context.pending_links.contains(&gid),
13868 "premise: the stream was refused, not re-linked"
13869 );
13870
13871 let mut received = Vec::new();
13872 for _ in 0..16 {
13873 frontend.readiness_mut().event.insert(Ready::WRITABLE);
13874 if frontend.readiness().filter_interest().is_writable() {
13875 frontend.writable(&mut context, EndpointClient(&mut router));
13876 }
13877 let mut buf = [0u8; 4096];
13878 if let Ok(n) = client.read(&mut buf) {
13879 received.extend_from_slice(&buf[..n]);
13880 }
13881 std::thread::yield_now();
13882 }
13883 assert!(
13884 received.starts_with(b"HTTP/1.1 503"),
13885 "the H1 client must read an error response, got {:?}",
13886 String::from_utf8_lossy(&received)
13887 );
13888 }
13889
13890 /// sozu-proxy/sozu#1632, connection-loss form: the backend connection
13891 /// goes away with the request encoded and parked, and
13892 /// `H2Shell::close` hands the stream to its frontend, whose
13893 /// `end_stream_decision` used to answer `Reconnect` on
13894 /// `!front.consumed`. The request must be answered with an error instead.
13895 ///
13896 /// TO SEE THIS RED: in `shared::end_stream_decision`, delete the
13897 /// `stream.front_bound_to_backend` branch. The stream is then re-linked:
13898 /// `a request encoded for a lost backend must not be re-linked`. Red on
13899 /// `d5161919`, verified 2026-09-28.
13900 #[test]
13901 fn a_request_encoded_for_a_lost_backend_is_answered_not_relinked() {
13902 let pool = make_pool_for_invariant_16();
13903 let mut backend = paced_backend(&pool, 0);
13904 let mut context = test_context(&pool);
13905 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
13906 let (gid, _) = park_an_encoded_request(&mut backend, &mut context, &mut router);
13907
13908 // What `H2Shell::close` does through `EndpointServer`: the frontend
13909 // ends the stream the lost backend carried.
13910 let (mut frontend, _peer) = test_h2_connection(&pool, None);
13911 frontend.core.end_stream(gid, &mut context);
13912
13913 assert!(
13914 !context.pending_links.contains(&gid),
13915 "a request encoded for a lost backend must not be re-linked"
13916 );
13917 assert_eq!(
13918 context.streams[gid].context.status,
13919 Some(502),
13920 "the client gets the answer a request that reached the backend gets"
13921 );
13922 }
13923
13924 /// sozu-proxy/sozu#1632, the property itself: no byte one backend
13925 /// connection's encoder produced may leave on another connection. Here
13926 /// the request encoded on backend A is offered to backend B, as a
13927 /// re-link would; B must refuse it and write nothing of A's.
13928 ///
13929 /// TO SEE THIS RED: in [`ConnectionH2::start_stream`], delete the
13930 /// `front.out` guard. B then sends A's frames — stream id 1 of A, a field
13931 /// block encoded against A's table: `backend B must not send what
13932 /// backend A encoded`. Red on `d5161919`, verified 2026-09-28.
13933 #[test]
13934 fn a_request_encoded_for_one_backend_never_reaches_another() {
13935 let pool = make_pool_for_invariant_16();
13936 let mut first = paced_backend(&pool, 0);
13937 let mut context = test_context(&pool);
13938 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
13939 let (gid, encoded) = park_an_encoded_request(&mut first, &mut context, &mut router);
13940 assert!(
13941 peer_frames(&encoded)
13942 .expect("whole frames")
13943 .iter()
13944 .any(|(kind, _, id, _)| *kind == 1 && *id == 1),
13945 "premise: backend A encoded a HEADERS frame for its stream 1"
13946 );
13947
13948 let mut second = paced_backend(&pool, usize::MAX);
13949 let started = second.start_stream(gid, &mut context);
13950 drive_both_ways(&mut second, &mut context, &mut router);
13951
13952 assert!(
13953 !second
13954 .socket
13955 .wire
13956 .windows(encoded.len())
13957 .any(|window| window == encoded.as_slice()),
13958 "backend B must not send what backend A encoded: {:?}",
13959 peer_frames(&second.socket.wire)
13960 );
13961 assert!(
13962 !started,
13963 "backend B must refuse a request encoded for another connection"
13964 );
13965 }
13966
13967 /// Queue on `gid`'s response side a 200 whose field block is `fields`,
13968 /// closed with END_STREAM, the way a relayed response reaches the write
13969 /// pass.
13970 fn queue_response(
13971 connection: &mut H2Shell<PacedSocket>,
13972 context: &mut Context<TestListener>,
13973 gid: GlobalStreamId,
13974 fields: Vec<(&'static [u8], kawa::Store)>,
13975 ) {
13976 let stream = &mut context.streams[gid];
13977 let kawa = &mut stream.back;
13978 kawa.detached.status_line = kawa::StatusLine::Response {
13979 version: kawa::Version::V20,
13980 code: 200,
13981 status: kawa::Store::Static(b"200"),
13982 reason: kawa::Store::Empty,
13983 };
13984 kawa.push_block(kawa::Block::StatusLine);
13985 for (name, value) in fields {
13986 kawa.push_block(kawa::Block::Header(kawa::Pair {
13987 key: kawa::Store::Static(name),
13988 val: value,
13989 }));
13990 }
13991 kawa.push_block(kawa::Block::Flags(kawa::Flags {
13992 end_body: false,
13993 end_chunk: false,
13994 end_header: true,
13995 end_stream: true,
13996 }));
13997 kawa.parsing_phase = kawa::ParsingPhase::Terminated;
13998 stream.state = StreamState::Unlinked;
13999 connection.core.readiness.arm_writable();
14000 }
14001
14002 /// A block dropped for exceeding `MAX_HEADER_LIST_SIZE` re-arms a table
14003 /// reset that must outlive the write pass when no other block of that
14004 /// pass carries it (sozu-proxy/sozu#1627, path 2). Stream 1's response
14005 /// inserts `x-a: 1`, then an `etag` value past the limit aborts it into a
14006 /// RST_STREAM, and it is the only header block of its pass. Stream 3's
14007 /// response, answered afterwards, encodes `x-a: 1` again, and the peer
14008 /// must read it: its block has to open with the reset.
14009 ///
14010 /// TO SEE THIS RED: in `ConnectionH2::poll_write_target`'s
14011 /// `H2WritePhase::End` arm, restore the pre-image
14012 /// `if converter_pass.size_update_emitted() { self.pending_table_size_update
14013 /// = None; }` instead of taking `converter_pass.pending_table_size_update()`.
14014 /// The reset is then lost with the pass: `the reset is still owed after
14015 /// the pass`, `left: None, right: Some(4096)`. Verified 2026-09-27.
14016 #[test]
14017 fn an_oversized_block_reset_outlives_the_pass_that_dropped_it() {
14018 let (_pool, mut connection, mut context, mut router, _peer) = two_requests_read(usize::MAX);
14019 let gid = |connection: &H2Shell<PacedSocket>, id: u32| {
14020 *connection
14021 .core
14022 .stream_table
14023 .streams()
14024 .get(&id)
14025 .expect("the stream is open")
14026 };
14027 let (first, third) = (gid(&connection, 1), gid(&connection, 3));
14028 let oversized = vec![b'v'; MAX_HEADER_LIST_SIZE + 1];
14029 queue_response(
14030 &mut connection,
14031 &mut context,
14032 first,
14033 vec![
14034 (b"x-a", kawa::Store::Static(b"1")),
14035 (b"etag", kawa::Store::from_slice(&oversized)),
14036 ],
14037 );
14038 drive_both_ways(&mut connection, &mut context, &mut router);
14039 assert!(
14040 peer_frames(&connection.socket.wire)
14041 .unwrap()
14042 .iter()
14043 .any(|(kind, _, id, _)| *kind == 3 && *id == 1),
14044 "premise: stream 1 was reset without HEADERS"
14045 );
14046 assert_eq!(
14047 connection.core.pending_table_size_update,
14048 Some(4096),
14049 "the reset is still owed after the pass"
14050 );
14051 queue_response(
14052 &mut connection,
14053 &mut context,
14054 third,
14055 vec![(b"x-a", kawa::Store::Static(b"1"))],
14056 );
14057 drive_both_ways(&mut connection, &mut context, &mut router);
14058 let (seen, status) = peer_header_blocks(&connection.socket.wire);
14059 assert_eq!(
14060 seen,
14061 vec![(3, true)],
14062 "the peer must decode stream 3's block with its own table"
14063 );
14064 assert_eq!(status, vec![b"200".to_vec()]);
14065 }
14066
14067 /// A response body 1000 octets past the default window, so one prepare
14068 /// takes the whole connection window and leaves the rest in the blocks.
14069 /// A `Store::Static`, because kawa 0.7.1's `Store::Alloc` arm of
14070 /// `Store::consume` subtracts the length before it adds the index back,
14071 /// which overflows in a debug build once the converter has split the
14072 /// chunk (CleverCloud/kawa#22).
14073 static WINDOW_SIZED_BODY: [u8; DEFAULT_INITIAL_WINDOW_SIZE as usize + 1000] =
14074 [b'b'; DEFAULT_INITIAL_WINDOW_SIZE as usize + 1000];
14075
14076 /// Queue on `gid`'s response side a 200 carrying `body`, closed with
14077 /// END_STREAM, the way a relayed response with a payload reaches the
14078 /// write pass.
14079 fn queue_response_with_body(
14080 connection: &mut H2Shell<PacedSocket>,
14081 context: &mut Context<TestListener>,
14082 gid: GlobalStreamId,
14083 body: &'static [u8],
14084 ) {
14085 let stream = &mut context.streams[gid];
14086 let kawa = &mut stream.back;
14087 kawa.detached.status_line = kawa::StatusLine::Response {
14088 version: kawa::Version::V20,
14089 code: 200,
14090 status: kawa::Store::Static(b"200"),
14091 reason: kawa::Store::Empty,
14092 };
14093 kawa.push_block(kawa::Block::StatusLine);
14094 kawa.push_block(kawa::Block::Flags(kawa::Flags {
14095 end_body: false,
14096 end_chunk: false,
14097 end_header: true,
14098 end_stream: false,
14099 }));
14100 kawa.push_block(kawa::Block::Chunk(kawa::Chunk {
14101 data: kawa::Store::Static(body),
14102 }));
14103 kawa.push_block(kawa::Block::Flags(kawa::Flags {
14104 end_body: true,
14105 end_chunk: false,
14106 end_header: false,
14107 end_stream: true,
14108 }));
14109 kawa.parsing_phase = kawa::ParsingPhase::Terminated;
14110 stream.state = StreamState::Unlinked;
14111 connection.core.readiness.arm_writable();
14112 }
14113
14114 /// The DATA payload octets `wire` carries for `stream_id`.
14115 fn data_on_wire(wire: &[u8], stream_id: u32) -> usize {
14116 peer_frames(wire)
14117 .unwrap()
14118 .iter()
14119 .filter(|(kind, _, id, _)| *kind == 0 && *id == stream_id)
14120 .map(|(_, _, _, payload)| payload.len())
14121 .sum()
14122 }
14123
14124 /// Stream 1's response body fills the whole connection send window in
14125 /// one prepare, the socket takes `budget` octets, and the stream is
14126 /// parked with the rest still queued. Its removal drops what the wire
14127 /// has not started.
14128 fn park_a_window_sized_body_on_stream_1(
14129 budget: usize,
14130 ) -> (
14131 H2Shell<PacedSocket>,
14132 Context<TestListener>,
14133 Router,
14134 GlobalStreamId,
14135 GlobalStreamId,
14136 ) {
14137 let (_pool, mut connection, mut context, mut router, _peer) = two_requests_read(budget);
14138 let gid = |connection: &H2Shell<PacedSocket>, id: u32| {
14139 *connection
14140 .core
14141 .stream_table
14142 .streams()
14143 .get(&id)
14144 .expect("the stream is open")
14145 };
14146 let (first, third) = (gid(&connection, 1), gid(&connection, 3));
14147 queue_response_with_body(&mut connection, &mut context, first, &WINDOW_SIZED_BODY);
14148 connection.core.readiness.event.insert(Ready::WRITABLE);
14149 connection.writable(&mut context, EndpointClient(&mut router));
14150 assert!(
14151 connection.socket.wire.len() == budget
14152 && connection.core.stream_table.expect_write().is_some(),
14153 "premise: stream 1's DATA was prepared and parked, the socket took {budget} octets"
14154 );
14155 assert_eq!(
14156 connection.core.flow_control.window(),
14157 0,
14158 "premise: preparing stream 1's body took the whole connection window"
14159 );
14160 (connection, context, router, first, third)
14161 }
14162
14163 /// RFC 9113 §6.9: the connection send window accounts for the DATA that
14164 /// was SENT. DATA prepared on stream 1 and dropped unsent when the peer
14165 /// resets it never reaches the peer, which will therefore never return
14166 /// that credit, so the connection must take it back itself; otherwise
14167 /// the window stays at zero for the life of the connection and stream 3's
14168 /// body never leaves (sozu-proxy/sozu#1641).
14169 ///
14170 /// TO SEE THIS RED: in `ConnectionH2::remove_dead_stream`, delete the
14171 /// `self.refund_parked_data(parked_data)` call. The removal then gives
14172 /// nothing back: `the removal itself gives the credit back`, `left: 0,
14173 /// right: 65535`. Without the fix at all, stream 3 also sends nothing
14174 /// (`left: 0, right: 1000`). Delete the `self.readiness.arm_writable()`
14175 /// in `ConnectionH2::refund_parked_data` instead: `and re-arms WRITABLE`
14176 /// fails. Verified 2026-09-28 (red on `5db53c60`).
14177 #[test]
14178 fn data_dropped_with_its_parked_stream_gives_the_connection_window_back() {
14179 let (mut connection, mut context, mut router, _first, third) =
14180 park_a_window_sized_body_on_stream_1(0);
14181 // A zero-progress pass may release WRITABLE; only the refund may
14182 // bring it back here.
14183 connection.core.readiness.interest.remove(Ready::WRITABLE);
14184 connection.socket.inbound.extend(RST_STREAM_1);
14185 for _ in 0..4 {
14186 connection.core.readiness.event.insert(Ready::READABLE);
14187 connection.readable(&mut context, EndpointClient(&mut router));
14188 }
14189 assert!(
14190 !connection.core.stream_table.streams().contains_key(&1),
14191 "premise: the RST removed stream 1"
14192 );
14193 assert_eq!(
14194 connection.core.flow_control.window(),
14195 DEFAULT_INITIAL_WINDOW_SIZE as i32,
14196 "the removal itself gives the credit back, before any write pass"
14197 );
14198 assert!(
14199 connection.core.readiness.interest.is_writable(),
14200 "and re-arms WRITABLE for the streams the closed window starved"
14201 );
14202 connection.socket.budget = usize::MAX;
14203 queue_response_with_body(&mut connection, &mut context, third, &[b'c'; 1000]);
14204 drive_both_ways(&mut connection, &mut context, &mut router);
14205 let wire = &connection.socket.wire;
14206 assert_eq!(data_on_wire(wire, 1), 0, "stream 1's DATA never left");
14207 assert_eq!(
14208 data_on_wire(wire, 3),
14209 1000,
14210 "stream 3's body must not wait on credit spent on dropped DATA"
14211 );
14212 assert_eq!(
14213 i64::from(connection.core.flow_control.window())
14214 + (data_on_wire(wire, 1) + data_on_wire(wire, 3)) as i64,
14215 i64::from(DEFAULT_INITIAL_WINDOW_SIZE),
14216 "the window is debited for the DATA octets sent, and only those"
14217 );
14218 }
14219
14220 /// The same drop without a removal: a forced termination clears the
14221 /// parked stream's queue and keeps the stream, which then sends its
14222 /// RST_STREAM. Both the connection's and the stream's credit for the
14223 /// dropped DATA come back (sozu-proxy/sozu#1641).
14224 ///
14225 /// TO SEE THIS RED: in `ConnectionH2::poll_write_target`'s
14226 /// `H2WritePhase::Start` arm, delete the `self.refund_parked_data(dropped)`
14227 /// call: `the connection window is back where it was before the
14228 /// prepare`, `left: 0, right: 65535`. Delete the `*parts.window` refund
14229 /// beside it instead: `the stream's window is back where it was before
14230 /// the prepare`, `left: 0, right: 65535`. Verified 2026-09-28 (red on
14231 /// `5db53c60`).
14232 #[test]
14233 fn data_dropped_from_a_live_parked_stream_gives_both_windows_back() {
14234 let (mut connection, mut context, mut router, first, _third) =
14235 park_a_window_sized_body_on_stream_1(0);
14236 crate::protocol::mux::answers::forcefully_terminate_answer(
14237 &mut context.streams[first],
14238 &mut connection.core.readiness,
14239 H2Error::InternalError,
14240 );
14241 connection.socket.budget = usize::MAX;
14242 connection.core.readiness.event.insert(Ready::WRITABLE);
14243 connection.writable(&mut context, EndpointClient(&mut router));
14244 assert_eq!(
14245 context.streams[first].front_window, DEFAULT_INITIAL_WINDOW_SIZE as i32,
14246 "the stream's window is back where it was before the prepare"
14247 );
14248 drive_both_ways(&mut connection, &mut context, &mut router);
14249 assert_eq!(
14250 data_on_wire(&connection.socket.wire, 1),
14251 0,
14252 "premise: stream 1's DATA never left"
14253 );
14254 assert_eq!(
14255 connection.core.flow_control.window(),
14256 DEFAULT_INITIAL_WINDOW_SIZE as i32,
14257 "the connection window is back where it was before the prepare"
14258 );
14259 }
14260
14261 /// The peer grows the connection window to 2^31-1 while stream 1's
14262 /// 65535 prepared octets sit parked, then resets the stream. The window
14263 /// that really stands is 2^31-1 plus those octets, past the RFC 9113
14264 /// §6.9.1 ceiling, and the refund is where that shows: the connection
14265 /// answers GOAWAY(FLOW_CONTROL_ERROR), as it does for an overflowing
14266 /// WINDOW_UPDATE, instead of asserting on what the peer sent.
14267 ///
14268 /// TO SEE THIS RED: in `H2FlowControl::refund_send_window`, put back a
14269 /// `debug_assert!(false, ..)` on the `checked_add` overflow: the test
14270 /// panics on `a refund of consumed credit cannot overflow the send
14271 /// window`, which is how `08730c9a` behaved. In
14272 /// `ConnectionH2::refund_parked_data`, answer `None` on the overflow
14273 /// instead of the GOAWAY: `exactly one GOAWAY`, `left: 0, right: 1`.
14274 /// Verified 2026-09-28.
14275 #[test]
14276 fn a_refund_the_peer_pushed_past_the_window_ceiling_is_a_flow_control_error() {
14277 let (mut connection, mut context, mut router, _first, _third) =
14278 park_a_window_sized_body_on_stream_1(0);
14279 // WINDOW_UPDATE(stream 0, 2^31-1), then RST_STREAM(1, CANCEL).
14280 connection
14281 .socket
14282 .inbound
14283 .extend(b"\x00\x00\x04\x08\x00\x00\x00\x00\x00\x7f\xff\xff\xff");
14284 connection.socket.inbound.extend(RST_STREAM_1);
14285 for _ in 0..4 {
14286 connection.core.readiness.event.insert(Ready::READABLE);
14287 connection.readable(&mut context, EndpointClient(&mut router));
14288 }
14289 assert_eq!(
14290 connection.core.flow_control.window(),
14291 i32::MAX,
14292 "premise: the peer's WINDOW_UPDATE was accepted, and the refund not applied"
14293 );
14294 connection.socket.budget = usize::MAX;
14295 drive_both_ways(&mut connection, &mut context, &mut router);
14296 let goaways: Vec<Vec<u8>> = peer_frames(&connection.socket.wire)
14297 .unwrap()
14298 .into_iter()
14299 .filter(|(kind, _, _, _)| *kind == 7)
14300 .map(|(_, _, _, payload)| payload)
14301 .collect();
14302 assert_eq!(goaways.len(), 1, "exactly one GOAWAY");
14303 assert_eq!(
14304 &goaways[0][4..8],
14305 &3u32.to_be_bytes(),
14306 "the GOAWAY carries FLOW_CONTROL_ERROR"
14307 );
14308 }
14309
14310 /// A second write pass that is stalled too resumes the parked stream,
14311 /// writes nothing and parks it again: every prepared DATA octet is still
14312 /// queued, so nothing is given back and both windows stay at zero
14313 /// (sozu-proxy/sozu#1641). Only DATA no longer queued is refunded.
14314 ///
14315 /// TO SEE THIS RED: in `ConnectionH2::poll_write_target`'s
14316 /// `H2WritePhase::Start` arm, refund `parked_data` instead of
14317 /// `parked_data - queued`: `left: 65535, right: 0`. Verified 2026-09-28.
14318 #[test]
14319 fn data_still_parked_after_a_second_stalled_pass_is_not_given_back() {
14320 let (mut connection, mut context, mut router, first, _third) =
14321 park_a_window_sized_body_on_stream_1(0);
14322 connection.core.readiness.event.insert(Ready::WRITABLE);
14323 connection.writable(&mut context, EndpointClient(&mut router));
14324 assert!(
14325 connection.socket.wire.is_empty()
14326 && connection.core.stream_table.expect_write().is_some(),
14327 "premise: the second pass resumed nothing and kept the park"
14328 );
14329 assert_eq!(
14330 connection.core.flow_control.window(),
14331 0,
14332 "the connection window still owes every queued DATA octet"
14333 );
14334 assert_eq!(
14335 context.streams[first].front_window, 0,
14336 "and so does the stream's"
14337 );
14338 }
14339
14340 /// A write that cuts stream 1's first DATA frame hands the rest of that
14341 /// frame to the ordered output queue, which sends it whole even after
14342 /// the stream is gone (sozu-proxy/sozu#1625). Only the frames behind it,
14343 /// which the wire never started, are given back: the window still
14344 /// accounts for exactly the DATA octets sent (sozu-proxy/sozu#1641).
14345 ///
14346 /// TO SEE THIS RED: in `ConnectionH2::remove_dead_stream`, give back the
14347 /// whole prepare instead of the parked DATA
14348 /// (`self.refund_parked_data(DEFAULT_INITIAL_WINDOW_SIZE as i32)`). The
14349 /// window then exceeds the peer's grant by the cut frame's payload:
14350 /// `left: 81919, right: 65535`. Without the fix at all it falls short by
14351 /// the dropped frames instead: `left: 16384, right: 65535`. Verified
14352 /// 2026-09-28 (red on `5db53c60`).
14353 #[test]
14354 fn data_a_partial_write_started_is_sent_whole_and_not_given_back() {
14355 // A 10-octet HEADERS (`:status: 200`), then 109 octets of the first
14356 // DATA frame: its header and 100 octets of payload.
14357 let budget = 10 + 9 + 100;
14358 let (mut connection, mut context, mut router, _first, _third) =
14359 park_a_window_sized_body_on_stream_1(budget);
14360 connection.socket.inbound.extend(RST_STREAM_1);
14361 for _ in 0..4 {
14362 connection.core.readiness.event.insert(Ready::READABLE);
14363 connection.readable(&mut context, EndpointClient(&mut router));
14364 }
14365 assert!(
14366 !connection.core.stream_table.streams().contains_key(&1),
14367 "premise: the RST removed stream 1"
14368 );
14369 connection.socket.budget = usize::MAX;
14370 drive_both_ways(&mut connection, &mut context, &mut router);
14371 let sent = data_on_wire(&connection.socket.wire, 1);
14372 assert_eq!(
14373 sent, 16_384,
14374 "the cut frame left whole, and nothing after it"
14375 );
14376 assert_eq!(
14377 i64::from(connection.core.flow_control.window()) + sent as i64,
14378 i64::from(DEFAULT_INITIAL_WINDOW_SIZE),
14379 "the window is debited for the DATA octets sent, and only those"
14380 );
14381 }
14382
14383 /// An H1 backend answers chunked with `Connection: close`, sends the
14384 /// last chunk and a first trailer line, then closes before the final
14385 /// CRLF. The trailer block can never be completed, and the converter
14386 /// defers a field block until its closing flags are queued
14387 /// (sozu-proxy/sozu#1627). `ConnectionH2::end_stream`'s
14388 /// `EndStreamAction::CloseDelimited` arm must not leave the stream
14389 /// waiting on that block: the chunked body was cut short, so it ends
14390 /// with RST_STREAM, exactly as `ConnectionH1::terminate_close_delimited`
14391 /// (`lib/src/protocol/mux/h1.rs`) does, and the unfinished trailer is
14392 /// dropped unencoded, so the next response still decodes on the peer.
14393 ///
14394 /// Red on `d12c3299`, which deferred the trailer with no way out: the
14395 /// stream stayed open with its trailer queued forever, `stream 1 must end
14396 /// with RST_STREAM`, frames `[(1, 0), (0, 0)]`. TO SEE THIS RED now: in
14397 /// `ConnectionH2::end_stream`, delete the `EndStreamAction::CloseDelimited`
14398 /// arm guarded by `body_size != kawa::BodySize::Empty`. The truncated
14399 /// response then ends silently: frames `[(1, 0), (0, 0), (0, 1)]`.
14400 /// Verified 2026-09-27.
14401 #[test]
14402 fn a_chunked_response_closed_inside_its_trailers_ends_with_a_reset() {
14403 let (_pool, mut connection, mut context, mut router, _peer) = two_requests_read(usize::MAX);
14404 let gid = |connection: &H2Shell<PacedSocket>, id: u32| {
14405 *connection
14406 .core
14407 .stream_table
14408 .streams()
14409 .get(&id)
14410 .expect("the stream is open")
14411 };
14412 let (first, third) = (gid(&connection, 1), gid(&connection, 3));
14413 {
14414 let stream = &mut context.streams[first];
14415 stream.context.keep_alive_backend = false;
14416 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";
14417 let kawa = &mut stream.back;
14418 kawa.storage.space()[..bytes.len()].copy_from_slice(bytes);
14419 kawa.storage.fill(bytes.len());
14420 kawa::h1::parse(kawa, &mut kawa::h1::NoCallbacks);
14421 assert_eq!(
14422 kawa.parsing_phase,
14423 kawa::ParsingPhase::Trailers,
14424 "premise: the backend stopped inside the trailers"
14425 );
14426 stream.state = StreamState::Unlinked;
14427 }
14428 connection.core.readiness.arm_writable();
14429 drive_both_ways(&mut connection, &mut context, &mut router);
14430 connection.core.end_stream(first, &mut context);
14431 drive_both_ways(&mut connection, &mut context, &mut router);
14432
14433 let frames: Vec<(u8, u8)> = peer_frames(&connection.socket.wire)
14434 .unwrap()
14435 .iter()
14436 .filter(|(_, _, id, _)| *id == 1)
14437 .map(|(kind, flags, _, _)| (*kind, flags & parser::FLAG_END_STREAM))
14438 .collect();
14439 assert_eq!(
14440 frames.last().map(|(kind, _)| *kind),
14441 Some(0x3),
14442 "stream 1 must end with RST_STREAM, frames {frames:?}"
14443 );
14444 assert!(
14445 !frames.iter().any(|(_, end_stream)| *end_stream != 0),
14446 "a truncated chunked body never ends cleanly: {frames:?}"
14447 );
14448 assert!(
14449 !connection.core.stream_table.streams().contains_key(&1),
14450 "stream 1 is retired"
14451 );
14452
14453 queue_response(
14454 &mut connection,
14455 &mut context,
14456 third,
14457 vec![(b"x-checksum", kawa::Store::Static(b"abc"))],
14458 );
14459 drive_both_ways(&mut connection, &mut context, &mut router);
14460 let (seen, status) = peer_header_blocks(&connection.socket.wire);
14461 assert_eq!(
14462 seen,
14463 vec![(1, true), (3, true)],
14464 "every block the peer reads decodes with its own table"
14465 );
14466 assert_eq!(status, vec![b"200".to_vec(), b"200".to_vec()]);
14467 }
14468
14469 /// An H1 backend answers `Content-Length: 10` with `Connection: close`,
14470 /// sends 4 body bytes, and its connection goes away without the EOF read
14471 /// path ending the body (a socket error or a backend timeout), so
14472 /// `ConnectionH2::end_stream` meets the response still in its body phase.
14473 /// Its `EndStreamAction::CloseDelimited` arm must end the stream with
14474 /// RST_STREAM: the body is 6 bytes short of its `content-length`, and
14475 /// only a body with neither `Content-Length` nor chunked coding is
14476 /// delimited by the close (RFC 9112 §6.3; RFC 9113 §8.1.1).
14477 ///
14478 /// Red on `d5161919`, whose guard covered chunked only: stream 1 ended
14479 /// with DATA carrying END_STREAM (sozu-proxy/sozu#1633).
14480 #[test]
14481 fn a_content_length_response_cut_short_ends_with_a_reset() {
14482 let (_pool, mut connection, mut context, mut router, _peer) = two_requests_read(usize::MAX);
14483 let first = *connection
14484 .core
14485 .stream_table
14486 .streams()
14487 .get(&1)
14488 .expect("the stream is open");
14489 {
14490 let stream = &mut context.streams[first];
14491 stream.context.keep_alive_backend = false;
14492 let bytes: &[u8] = b"HTTP/1.1 200 OK\r\nContent-Length: 10\r\n\r\nabcd";
14493 let kawa = &mut stream.back;
14494 kawa.storage.space()[..bytes.len()].copy_from_slice(bytes);
14495 kawa.storage.fill(bytes.len());
14496 kawa::h1::parse(kawa, &mut kawa::h1::NoCallbacks);
14497 assert!(
14498 kawa.is_main_phase() && !kawa.is_terminated() && kawa.expects == 6,
14499 "premise: the body is 6 bytes short, {:?}",
14500 kawa.parsing_phase
14501 );
14502 stream.state = StreamState::Unlinked;
14503 }
14504 connection.core.readiness.arm_writable();
14505 drive_both_ways(&mut connection, &mut context, &mut router);
14506 connection.core.end_stream(first, &mut context);
14507 drive_both_ways(&mut connection, &mut context, &mut router);
14508
14509 let frames: Vec<(u8, u8)> = peer_frames(&connection.socket.wire)
14510 .unwrap()
14511 .iter()
14512 .filter(|(_, _, id, _)| *id == 1)
14513 .map(|(kind, flags, _, _)| (*kind, flags & parser::FLAG_END_STREAM))
14514 .collect();
14515 assert_eq!(
14516 frames.last().map(|(kind, _)| *kind),
14517 Some(0x3),
14518 "stream 1 must end with RST_STREAM, frames {frames:?}"
14519 );
14520 assert!(
14521 !frames.iter().any(|(_, end_stream)| *end_stream != 0),
14522 "a truncated Content-Length body never ends cleanly: {frames:?}"
14523 );
14524 }
14525
14526 /// The same block dropped while its stream stays parked:
14527 /// `forcefully_terminate_answer` (a backend that closed mid-response)
14528 /// clears the parked stream's `kawa.out`, which held the encoded block.
14529 /// Stream 1 then ends with a RST_STREAM, and stream 3's block must still
14530 /// decode on the peer (sozu-proxy/sozu#1627).
14531 ///
14532 /// TO SEE THIS RED: in `ConnectionH2::poll_write_target`'s
14533 /// `H2WritePhase::Start` arm, append `&& false` to the `.is_empty()` that
14534 /// judges a park still in place. The peer then fails on stream 3's block:
14535 /// `left: [(3, false)], right: [(3, true)]`, and this is the only test
14536 /// that fails. Verified 2026-09-27.
14537 #[test]
14538 fn a_header_block_cleared_from_a_parked_stream_keeps_the_peer_table_in_sync() {
14539 let (seen, status) =
14540 peer_reads_after_a_dropped_parked_block(|connection, context, _router| {
14541 let gid = *connection
14542 .core
14543 .stream_table
14544 .streams()
14545 .get(&1)
14546 .expect("stream 1 is open");
14547 forcefully_terminate_answer(
14548 &mut context.streams[gid],
14549 &mut connection.core.readiness,
14550 H2Error::InternalError,
14551 );
14552 assert!(
14553 context.streams[gid].back.out.is_empty()
14554 && connection.core.stream_table.expect_write().is_some(),
14555 "premise: stream 1 stays parked with its block cleared"
14556 );
14557 });
14558 assert_eq!(
14559 seen,
14560 vec![(3, true)],
14561 "the peer must decode stream 3's block with its own table"
14562 );
14563 assert_eq!(status, vec![b"404".to_vec()], "and read what was encoded");
14564 }
14565
14566 /// Resume-path bytes are not pass progress.
14567 ///
14568 /// `finalize_write` reads `bytes_written_this_pass > 0` as
14569 /// `made_progress`, and `made_progress && any_pending_back()` is LIFECYCLE
14570 /// §9 invariant 16: it RETAINS `Ready::WRITABLE` because a pass that moved
14571 /// bytes and left blocks behind is a voluntary scheduler yield that will
14572 /// resume itself. The resume path's drain is not that. It happens in
14573 /// `H2WritePhase::Resume`, before the scheduler pass opens at all; it is a
14574 /// socket-backpressure catch-up rather than a
14575 /// yield, and the stranded blocks it leaves belong to a stream the
14576 /// converter never reached this pass. Counting it as progress would keep
14577 /// WRITABLE armed on a connection whose scheduler pass did nothing, and
14578 /// the next tick would find the same nothing — a spin, not a resume.
14579 ///
14580 /// So `resume_bytes` is deliberately NOT accumulated into
14581 /// `total_bytes_written`: only the main loop's `stream_bytes` is. With a
14582 /// drained resume and a zero-byte scheduler pass, the correct answer is
14583 /// `FinalizeAction::Quiesce`, which withdraws `Ready::WRITABLE` and waits
14584 /// for an external trigger.
14585 ///
14586 /// TO SEE THIS RED: in `begin_scheduler_pass`, seed the pass total from
14587 /// the resume counter — insert `pass.total_bytes_written =
14588 /// pass.resume_bytes;` immediately before
14589 /// `pass.adopt_scheduler_pass(converter_pass, order, census);`, which is
14590 /// where the pre-image seeded `let mut total_bytes_written: usize = 0;`
14591 /// ahead of its per-stream loop. Both counters are fields of
14592 /// `H2WritePass` and keeping them apart is the whole point, so the shape
14593 /// a refactor unifying the two flush paths would reach for is exactly this
14594 /// one statement. Measured: this test fails on its own first
14595 /// assertion, `a pass whose scheduler loop wrote nothing must withdraw
14596 /// WRITABLE interest`, with `left: Writable | Error | Hup, right: Error |
14597 /// Hup`, and it is the ONLY failure in the suite.
14598 /// No other test reaches the resume path at all, which is why the blast
14599 /// radius is one.
14600 #[test]
14601 fn resume_path_bytes_do_not_make_the_pass_progress() {
14602 let pool = make_pool_for_invariant_16();
14603 let (mut connection, mut context, gid, _peer) = writable_fixture(
14604 &pool,
14605 &[FIRST_BLOCK, SECOND_BLOCK],
14606 &[(TOTAL_QUEUED, SocketResult::Continue)],
14607 );
14608 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
14609
14610 connection
14611 .core
14612 .stream_table
14613 .set_expect_write(Some(H2StreamId::Other {
14614 id: REGISTERED_STREAM_ID,
14615 gid,
14616 }));
14617 // A pending back-buffer, which is the OTHER half of invariant 16's
14618 // condition. The flush drains `out` and never touches
14619 // `blocks`, so this block survives the resume and makes
14620 // `any_stream_has_pending_back` answer true — leaving `made_progress`
14621 // as the single input that decides Quiesce against RetainPendingBack.
14622 context.streams[gid]
14623 .back
14624 .blocks
14625 .push_back(kawa::Block::StatusLine);
14626 assert_prepare_gate_is_shut(&context, gid);
14627 assert!(
14628 connection.core.readiness.interest.is_writable(),
14629 "premise: WRITABLE interest must start armed, or the withdrawal \
14630 below is unobservable, got {:?}",
14631 connection.core.readiness
14632 );
14633
14634 let result = connection.writable(&mut context, EndpointClient(&mut router));
14635
14636 assert_eq!(
14637 connection.core.readiness.interest,
14638 Ready::HUP | Ready::ERROR,
14639 "a pass whose scheduler loop wrote nothing must withdraw WRITABLE \
14640 interest: the resume path's drain is socket catch-up, not the \
14641 voluntary yield invariant 16 retains the bit for"
14642 );
14643 assert_eq!(
14644 context.streams[gid].metrics.bout, TOTAL_QUEUED,
14645 "premise: the resume path really did move every queued byte — \
14646 without that this test would assert Quiesce over a pass that \
14647 simply had nothing to write"
14648 );
14649 assert!(
14650 !context.streams[gid].back.blocks.is_empty(),
14651 "premise: the pending back-buffer must survive the pass, or \
14652 any_stream_has_pending_back answers false and RetainPendingBack \
14653 is unreachable whatever made_progress says"
14654 );
14655 assert_eq!(
14656 connection.core.stream_table.expect_write(),
14657 None,
14658 "a drained resume clears the park before the scheduler pass"
14659 );
14660 assert_eq!(
14661 connection.socket.vectored_calls, 1,
14662 "the scheduler pass must find nothing to write: the resume drained \
14663 `out`, and the prepare gate is shut"
14664 );
14665 assert!(
14666 matches!(result, MuxResult::Continue),
14667 "a quiesced write pass continues, got {result:?}"
14668 );
14669 }
14670
14671 // ── One pass, one prepare per stream ────────────────────────────────
14672 //
14673 // The four tests above pin the FLUSH half of a write pass. This one pins
14674 // the PREPARE half against the same refactor: `write_streams` runs the
14675 // gate at `if kawa.is_main_phase() || ...` — `kawa.prepare`, the
14676 // `*parts.window` debit, `flow_control.consume_send_window`,
14677 // `census.note_fired` and the `freshly_emitted_rsts` push — ONCE per
14678 // stream per pass, in `H2WritePhase::Prepare` and outside the round-again
14679 // `H2WritePhase::Flush` re-enters. A restructure that lifted those rounds
14680 // up beside the prepare would debit the send window twice for
14681 // one stream's single scheduling turn: a connection would believe it had
14682 // spent credit it never put on the wire, and would stall itself short of
14683 // the peer's real window.
14684 //
14685 // MEASURED, and it corrects the obvious way to write this test. A narrow
14686 // per-stream window does NOT expose a double prepare. With
14687 // `create_stream(ulid, 64)` against a 200-byte body and a two-round flush,
14688 // production and an edit that visits every stream twice in one pass give
14689 // the same `delta=64 vectored_calls=2 blocks_left=1 bout=73`. The send
14690 // window is SELF-LIMITING: `*parts.window =
14691 // parts.window.saturating_sub(consumed)` zeroes the stream's credit, the
14692 // second prepare computes `min(0, connection_window) == 0`, the converter's
14693 // `self.window > 0` gate returns the chunk untouched, and
14694 // `consumed = 0 - 0 = 0`. A non-incremental prepare always takes every byte
14695 // the window allows, so a second one has nothing left to take.
14696 //
14697 // INVISIBLE TO THOSE OBSERVABLES IS NOT HARMLESS, and the difference is
14698 // the reason this block is worth reading. Two things DO move on that
14699 // second visit, measured on the same fixture:
14700 //
14701 // - `converter.rs`'s flow-control-stall arm fires. It is the arm reached
14702 // BECAUSE the window is zero, and its `incr!(names::h2::FLOW_CONTROL_STALL)`
14703 // is unconditional once there — on `Position::Client` it also fires
14704 // `incr!(names::backend::FLOW_CONTROL_PAUSED)`. Measured with a `panic!`
14705 // planted on that `incr!`: the fixture does NOT reach it under
14706 // production and DOES under the double visit. Those two counters are
14707 // dashboard signals for a real RFC 9113 §6.9 condition, so a re-entrant
14708 // write loop would inflate them with stalls that never happened on the
14709 // wire — an operator would read backpressure into a connection that had
14710 // none.
14711 // - `context.debug.push(DebugEvent::S(..))` sits OUTSIDE the prepare
14712 // gate's closing brace, so it fires once per VISIT, not once per
14713 // prepare: measured `s_events` 1 -> 2 while `io_events` stays 2. Every
14714 // extra entry evicts a real one from the bounded `DebugHistory` ring.
14715 //
14716 // So the correct claim is narrow: the BYTES are idempotent, the
14717 // TELEMETRY is not.
14718 //
14719 // And the bytes are idempotent by FOUR independent mechanisms, only one of
14720 // which is the window — a future reader must not conclude the window is
14721 // the guard:
14722 //
14723 // 1. `consumed` debits `*parts.window` AND `flow_control`, which is the
14724 // self-limiting above;
14725 // 2. `H2Scheduler::note_fired` gates its bucket's round-robin lead on
14726 // `consumed > 0`, so a zero-consumption re-entry cannot advance the
14727 // cursor;
14728 // 3. the `rst_sent` `HashSet` in `H2StreamTable` dedups RST_STREAM.
14729 // This one is load-bearing rather than incidental:
14730 // `H2BlockConverter::initialize` pushes a RST_STREAM frame on EVERY
14731 // prepare of an errored kawa with no window gate whatsoever, so
14732 // without that set a second visit would put a duplicate RST on the
14733 // wire regardless of the send window;
14734 // 4. `std::mem::take(&mut parts.context.headers_response)` drains the
14735 // response-header edits, so a re-entry cannot apply them twice.
14736 //
14737 // The RFC 9218 §4 incremental yield is the one path that stops a prepare
14738 // with BOTH window and blocks remaining: `H2BlockConverter`'s DATA arm
14739 // returns `can_continue && !yield_after_data`, and with
14740 // `incremental_mode && incremental_peer_count > 1` it yields after a
14741 // single DATA frame while the window is still wide open. That is why this
14742 // test registers a same-urgency incremental PAIR rather than one stream,
14743 // and why the body must exceed `SETTINGS_MAX_FRAME_SIZE` (16 384) so one
14744 // frame cannot carry it.
14745
14746 /// A response body larger than the default 16 384-byte
14747 /// `SETTINGS_MAX_FRAME_SIZE`, so one DATA frame cannot carry it and the
14748 /// incremental yield leaves the remainder in `blocks`.
14749 static YIELDING_BODY: [u8; 40_000] = [b'x'; 40_000];
14750
14751 /// Payload bytes one yielded prepare converts: the default
14752 /// `SETTINGS_MAX_FRAME_SIZE`, which bounds the frame before the window does.
14753 const YIELDED_PAYLOAD: i32 = 16_384;
14754
14755 /// Bytes one yielded prepare puts in `kawa.out`: the DATA frame header
14756 /// plus its payload.
14757 const YIELDED_FRAME: usize = parser::FRAME_HEADER_SIZE + YIELDED_PAYLOAD as usize;
14758
14759 /// Two registered, same-urgency INCREMENTAL server streams, each carrying
14760 /// `YIELDING_BODY` as one un-prepared chunk and each granted a send window
14761 /// far wider than a single frame.
14762 ///
14763 /// The width is the point: a narrow window would make the prepare stop
14764 /// because it ran out of credit, and a second prepare would then be a
14765 /// no-op whether or not the code is correct. Here the prepare stops
14766 /// because it YIELDED, so credit survives it and a second one would spend
14767 /// more.
14768 fn incremental_pair_fixture(
14769 pool: &Rc<RefCell<Pool>>,
14770 script: &[(usize, SocketResult)],
14771 ) -> (
14772 H2Shell<BackpressuredTlsSocket>,
14773 Context<TestListener>,
14774 [GlobalStreamId; 2],
14775 std::net::TcpStream,
14776 ) {
14777 let (mut connection, peer) = connection_with_backpressure(pool, 0, 0, H2State::Header);
14778 connection.socket.vectored_script = script.iter().copied().collect();
14779 let mut context = test_context(pool);
14780 let mut gids = [0usize; 2];
14781 for (slot, stream_id) in [1u32, 3u32].into_iter().enumerate() {
14782 let gid = context
14783 .create_stream(Ulid::generate(), 1 << 16)
14784 .expect("test context must create a stream");
14785 gids[slot] = gid;
14786 connection
14787 .core
14788 .stream_table
14789 .register(stream_id, gid, connection.core.now);
14790 // Same urgency for both, so they share one bucket and the pass
14791 // census answers `incremental_peer_count == 2`. At 1 the
14792 // converter's solo-bucket guard suppresses the yield and the
14793 // prepare drains to the window, which is the configuration this
14794 // test exists to avoid.
14795 connection.core.scheduler.push_priority(
14796 stream_id,
14797 parser::PriorityPart::Rfc9218 {
14798 urgency: 3,
14799 incremental: true,
14800 },
14801 );
14802 let back = &mut context.streams[gid].back;
14803 back.parsing_phase = kawa::ParsingPhase::Body;
14804 back.blocks.push_back(kawa::Block::Chunk(kawa::Chunk {
14805 data: kawa::Store::Static(&YIELDING_BODY),
14806 }));
14807 }
14808 (connection, context, gids, peer)
14809 }
14810
14811 /// One write pass debits the connection send window once per stream, even
14812 /// when that stream's flush goes round twice.
14813 ///
14814 /// Stream 1's flush is scripted to take two rounds — `(20, WouldBlock)`
14815 /// then the rest — so the pass contains exactly the round-again shape the
14816 /// flush tests above pin, and this test says the prepare does NOT come
14817 /// along for the second round. Stream 3's single round is the control:
14818 /// both streams yield after one frame, and both must cost the same credit.
14819 ///
14820 /// WHAT THE EXISTING FIXTURE CANNOT SEE. `a_partial_write_reporting_would_block_continues_the_pass`
14821 /// drives the identical two-round flush and stays green under every edit
14822 /// below: its stream's parsing phase leaves the prepare gate shut by
14823 /// construction, so it never runs a prepare at all and no amount of
14824 /// re-preparing is representable in it. Its through-`writable()` twin
14825 /// cannot see it either, for the same reason.
14826 ///
14827 /// TO SEE THIS RED: in `begin_scheduler_pass`, insert
14828 /// `let order: Vec<StreamId> = order.iter().chain(order.iter()).copied().collect();`
14829 /// immediately before `pass.adopt_scheduler_pass(converter_pass, order, census);`,
14830 /// so each stream takes a second turn — prepare included — within one pass.
14831 /// Measured: this test fails on its own first assertion, `one write pass
14832 /// must debit the connection send window once per stream`, with
14833 /// `left: 65535, right: 32768`, and it is the ONLY failure in the suite.
14834 /// The four frames the mutated pass emits instead
14835 /// of two also move `vectored_calls` from 3 to 5 and each stream's `bout`
14836 /// from 16 393 to 32 786 / 32 785, so the window is not a privileged
14837 /// witness here — but it is the only one that names the SPENT CREDIT, and
14838 /// a window debited for bytes the wire never carried is the failure that
14839 /// stalls a connection. The same assertion is also the third failure under
14840 /// the pass-terminator edit documented on
14841 /// `a_partial_write_reporting_would_block_continues_the_pass_through_writable`,
14842 /// where it reads `left: 16384, right: 32768` because stream 1's stall
14843 /// ends the pass before stream 3 is prepared.
14844 ///
14845 /// This is NOT keyed on the partial write, and the name says so. The
14846 /// inversion makes the rounds visible — `H2WritePhase::Flush` is re-entered
14847 /// once per round — but the prepare is not in that phase to be repeated:
14848 /// it lives in `H2WritePhase::Prepare`, which a round-again never returns
14849 /// to. Forcing a second visit of the whole order is the smallest honest
14850 /// statement of the same bug, and it is what a restructure that folded the
14851 /// two phases together would actually produce.
14852 #[test]
14853 fn a_yielded_incremental_stream_is_prepared_once_per_pass() {
14854 let pool = make_pool_for_invariant_16();
14855 let (mut connection, mut context, gids, _peer) = incremental_pair_fixture(
14856 &pool,
14857 &[
14858 // Stream 1: a partial write that reports WouldBlock, then the
14859 // remainder. Stream 3: one round.
14860 (20, SocketResult::WouldBlock),
14861 (99_999, SocketResult::Continue),
14862 (99_999, SocketResult::Continue),
14863 ],
14864 );
14865 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
14866
14867 let window_before = connection.core.flow_control.window();
14868 assert_eq!(
14869 window_before, DEFAULT_INITIAL_WINDOW_SIZE as i32,
14870 "premise: the connection send window starts at the RFC 9113 \
14871 default, so the debit below is the pass's doing"
14872 );
14873 for gid in gids {
14874 assert!(
14875 context.streams[gid].back.is_main_phase(),
14876 "premise: the prepare gate must be OPEN for both streams, or \
14877 no window is spent at all"
14878 );
14879 }
14880
14881 let result = connection.writable(&mut context, EndpointClient(&mut router));
14882
14883 assert_eq!(
14884 window_before - connection.core.flow_control.window(),
14885 2 * YIELDED_PAYLOAD,
14886 "one write pass must debit the connection send window once per \
14887 stream: a second prepare would spend credit for bytes this pass \
14888 never put on the wire"
14889 );
14890 assert_eq!(
14891 connection.socket.vectored_calls, 3,
14892 "the pass must be two flush rounds for stream 1 and one for \
14893 stream 3 — without the two-round flush this test would not be \
14894 asserting anything about re-entry"
14895 );
14896 for gid in gids {
14897 assert_eq!(
14898 context.streams[gid].metrics.bout, YIELDED_FRAME,
14899 "each yielded prepare puts exactly one DATA frame on the wire"
14900 );
14901 assert!(
14902 !context.streams[gid].back.blocks.is_empty(),
14903 "premise: the yield must leave the rest of the body queued — \
14904 with empty blocks a second prepare could not spend anything \
14905 and this test could not fail"
14906 );
14907 }
14908 assert!(
14909 matches!(result, MuxResult::Continue),
14910 "a yielded write pass continues, got {result:?}"
14911 );
14912 }
14913
14914 /// The send window granted to the stream that makes this pass's progress.
14915 /// Narrower than `YIELDING_BODY`, so its prepare converts
14916 /// `PARTIAL_STREAM_WINDOW` payload bytes and leaves the rest in `blocks`.
14917 const PARTIAL_STREAM_WINDOW: u32 = 64;
14918
14919 /// Bytes that stream reaches the socket with: the DATA frame header plus
14920 /// its window-capped payload.
14921 const PARTIAL_FRAME: usize = parser::FRAME_HEADER_SIZE + PARTIAL_STREAM_WINDOW as usize;
14922
14923 /// A pass's byte total accumulates across streams: a later stream that
14924 /// writes nothing must not erase what an earlier one delivered.
14925 ///
14926 /// `pass.total_bytes_written.saturating_add(pass.stream_bytes)` in
14927 /// `H2WritePhase::Flush` is the whole of it, and nothing pinned it. `finalize_write` hands the
14928 /// total to `h2_close::finalize_action` as `made_progress`, and
14929 /// `made_progress && any_pending_back()` is LIFECYCLE §9 invariant 16 — the
14930 /// arm that RETAINS `Ready::WRITABLE` for a pass that moved bytes and left
14931 /// blocks behind. Drop the accumulation and a two-stream pass reports the
14932 /// LAST stream's count: stream 1 delivers a frame and keeps its remainder
14933 /// queued, stream 3 is eligible but window-blocked and writes nothing, the
14934 /// total reads 0, `expect_write` is parked on neither so `Parked` cannot
14935 /// mask it, `RetainPendingBack` is skipped and the pass falls through to
14936 /// `Quiesce`. `Quiesce` strips `Ready::WRITABLE`; edge-triggered epoll
14937 /// never re-fires; stream 1's body is stranded in `blocks` with nothing
14938 /// scheduled to drain it. That is a hung response, not a lost metric.
14939 ///
14940 /// The two streams differ only in their send window, which is what puts
14941 /// one on each side of the accumulation: 64 bytes buys stream 1 one
14942 /// window-capped DATA frame, and 0 buys stream 3 the converter's
14943 /// flow-control-stall arm, which returns the chunk untouched and leaves
14944 /// `kawa.out` empty so its flush never calls the socket at all.
14945 ///
14946 /// TO SEE THIS RED: in `poll_write_target`'s `H2WritePhase::Flush` arm,
14947 /// change the accumulation
14948 /// `pass.total_bytes_written = pass.total_bytes_written.saturating_add(pass.stream_bytes);`
14949 /// into `pass.total_bytes_written = pass.stream_bytes;` — the shape a
14950 /// rewrite that folds the resume and main flush paths into one per-stream
14951 /// counter naturally reaches for. Measured: this test fails on its own first
14952 /// assertion, `a pass that delivered bytes and left blocks queued must
14953 /// RETAIN WRITABLE interest`, with `left: Error | Hup, right: Writable |
14954 /// Error | Hup`, and it is the only failure in the suite. Measured without
14955 /// this test in the tree, that same edit leaves every other test green —
14956 /// which is what makes this one worth its lines.
14957 #[test]
14958 fn a_later_zero_byte_stream_does_not_erase_the_pass_progress() {
14959 let pool = make_pool_for_invariant_16();
14960 let (mut connection, _peer) = connection_with_backpressure(&pool, 0, 0, H2State::Header);
14961 // One entry: stream 1's frame. Stream 3 puts nothing in `kawa.out`, so
14962 // its flush loop never runs and never reaches the socket.
14963 connection.socket.vectored_script =
14964 [(99_999, SocketResult::Continue)].into_iter().collect();
14965 let mut context = test_context(&pool);
14966 let mut gids = [0usize; 2];
14967 for (slot, (stream_id, window)) in [(1u32, PARTIAL_STREAM_WINDOW), (3u32, 0)]
14968 .into_iter()
14969 .enumerate()
14970 {
14971 let gid = context
14972 .create_stream(Ulid::generate(), window)
14973 .expect("test context must create a stream");
14974 gids[slot] = gid;
14975 connection
14976 .core
14977 .stream_table
14978 .register(stream_id, gid, connection.core.now);
14979 let back = &mut context.streams[gid].back;
14980 back.parsing_phase = kawa::ParsingPhase::Body;
14981 back.blocks.push_back(kawa::Block::Chunk(kawa::Chunk {
14982 data: kawa::Store::Static(&YIELDING_BODY),
14983 }));
14984 }
14985 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
14986
14987 assert!(
14988 connection.core.readiness.interest.is_writable(),
14989 "premise: WRITABLE interest starts armed, so retaining it is not \
14990 vacuous, got {:?}",
14991 connection.core.readiness
14992 );
14993
14994 let result = connection.writable(&mut context, EndpointClient(&mut router));
14995
14996 assert_eq!(
14997 connection.core.readiness.interest,
14998 Ready::WRITABLE | Ready::HUP | Ready::ERROR,
14999 "a pass that delivered bytes and left blocks queued must RETAIN \
15000 WRITABLE interest (LIFECYCLE §9 invariant 16): stripping it \
15001 strands the remainder, because edge-triggered epoll will not \
15002 re-fire on its own"
15003 );
15004 assert_eq!(
15005 context.streams[gids[0]].metrics.bout, PARTIAL_FRAME,
15006 "premise: the FIRST stream really did deliver a frame — the \
15007 accumulation has nothing to preserve otherwise"
15008 );
15009 assert_eq!(
15010 context.streams[gids[1]].metrics.bout, 0,
15011 "premise: the SECOND stream really did write nothing, so it is the \
15012 stream whose count would overwrite the total"
15013 );
15014 assert_eq!(
15015 connection.socket.vectored_calls, 1,
15016 "only the first stream reaches the socket: a zero window leaves \
15017 `kawa.out` empty and its flush loop never runs"
15018 );
15019 for gid in gids {
15020 assert!(
15021 !context.streams[gid].back.blocks.is_empty(),
15022 "premise: both streams must leave blocks queued, or \
15023 any_pending_back answers false and RetainPendingBack is \
15024 unreachable whatever the byte total says"
15025 );
15026 }
15027 assert_eq!(
15028 connection.core.stream_table.expect_write(),
15029 None,
15030 "premise: nothing is parked, so `Parked` cannot decide this pass \
15031 ahead of the byte total"
15032 );
15033 assert!(
15034 matches!(result, MuxResult::Continue),
15035 "a retained write pass continues, got {result:?}"
15036 );
15037 }
15038
15039 // ── force_disconnect's server arm with records pending ──────────────
15040 //
15041 // `h2_close::force_disconnect_action` is exhaustively tabled over its two
15042 // booleans in that module. Nothing called `ConnectionH2::force_disconnect`
15043 // itself, so the CALLER half — the `H2Shell::tls_wants_write` query
15044 // that feeds the table and the wiring of `ReArmAndContinue` to interest,
15045 // flush and return value — had no coverage. #1454 asks for this site by
15046 // name.
15047 //
15048 // Shape targeted: a SINGLE query, not a triple. `force_disconnect` reads
15049 // the answer once into `tls_wants_write`, above its `match` and therefore
15050 // for both position arms, feeds the table and reports the same value from
15051 // every debug line; there is no `H2Shell::flush_tls_records` here and
15052 // so no second question to reconstruct.
15053
15054 /// A server connection whose TLS records are still buffered re-arms
15055 /// instead of closing the session.
15056 ///
15057 /// `MuxResult::CloseSession` triggers `shutdown(Write)`, which sends FIN
15058 /// and destroys whatever rustls is still holding — the client reads a
15059 /// truncated response. So the live-peer, records-pending row of
15060 /// `force_disconnect_action` must keep WRITABLE and let the writable path
15061 /// flush.
15062 ///
15063 /// TO SEE THIS RED: in `force_disconnect`'s `Position::Server` arm, pass
15064 /// `false` in place of `tls_wants_write` to `force_disconnect_action`.
15065 /// Measured: this test fails on its own first assertion, `a server still
15066 /// holding TLS records must not close: CloseSession sends FIN and destroys
15067 /// them, got CloseSession`, while `h2_close`'s own
15068 /// `force_disconnect_arm_is_exhaustive` stays green — the table is not the
15069 /// caller, which is why this test exists. Passing `true` instead would NOT
15070 /// redden this test; catching that constant ON THIS DIRECT PATH is what
15071 /// the sibling below is for — it is not the suite's only witness against
15072 /// `true`, and its own doc says which other test is.
15073 ///
15074 /// The interest assertion has its own recipe: delete
15075 /// `self.readiness.interest = Ready::WRITABLE | Ready::HUP | Ready::ERROR;`
15076 /// from the same arm. Measured: `the re-arm must restore WRITABLE interest
15077 /// so the writable path is called again to flush`, `left: Hup, right:
15078 /// Writable | Error | Hup`. That recipe only bites because the test
15079 /// withdraws the bit first — asserting it against the fixture's own
15080 /// constructor argument passed under this very mutation.
15081 #[test]
15082 fn force_disconnect_re_arms_while_tls_records_are_pending() {
15083 let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
15084 // A kernel that accepts nothing, so the records outlive any flush.
15085 let (mut connection, _peer) = connection_with_backpressure(&pool, 2, 0, H2State::GoAway);
15086
15087 assert!(
15088 connection.socket.socket_wants_write(),
15089 "premise: this harness must report buffered TLS records"
15090 );
15091 assert!(
15092 !connection.core.peer_gone_after_final_goaway(),
15093 "premise: the peer must still be live, or the table short-circuits \
15094 to CloseSession on the first boolean and the record count is never \
15095 read"
15096 );
15097 // Withdraw WRITABLE interest first. `connection_with_backpressure`
15098 // hands `WRITABLE | HUP | ERROR` to `ConnectionH2::new` as the
15099 // connection's INTEREST, so asserting on it untouched would be reading
15100 // this fixture's own argument back and would hold even if the re-arm
15101 // arm never assigned it. Starting from `HUP` alone makes the
15102 // assignment the only thing that can restore the bit.
15103 connection.core.readiness.interest = Ready::HUP;
15104 assert!(
15105 !connection.core.readiness.event.is_writable(),
15106 "premise: the WRITABLE event must start clear, so the signal below \
15107 is the only thing that can set it, got {:?}",
15108 connection.core.readiness
15109 );
15110
15111 let result = connection.force_disconnect();
15112
15113 assert!(
15114 matches!(result, MuxResult::Continue),
15115 "a server still holding TLS records must not close: CloseSession \
15116 sends FIN and destroys them, got {result:?}"
15117 );
15118 assert_eq!(
15119 connection.core.readiness.interest,
15120 Ready::WRITABLE | Ready::HUP | Ready::ERROR,
15121 "the re-arm must restore WRITABLE interest so the writable path is \
15122 called again to flush, got {:?}",
15123 connection.core.readiness
15124 );
15125 assert!(
15126 connection.core.readiness.event.is_writable(),
15127 "the re-arm must signal the edge-triggered WRITABLE event, or the \
15128 event loop never revisits this connection, got {:?}",
15129 connection.core.readiness
15130 );
15131 }
15132
15133 /// The same call with nothing buffered closes the session.
15134 ///
15135 /// The other row of the table. It does NOT rest on being the only thing
15136 /// that catches a hard-wired `true`: the suite already caught that
15137 /// constant without it. Measured on that mutation, TWO tests fail — this
15138 /// test AND
15139 /// `a_flush_that_succeeds_closes_within_one_writable_call` above, which
15140 /// predates it and reaches `force_disconnect` through `writable()`'s
15141 /// GoAway fall-through, failing with `a drained flush must reach the
15142 /// disconnect in the SAME writable call, not defer it to another tick:
15143 /// got Continue`.
15144 ///
15145 /// What this test adds is the PATH, not the count: it calls
15146 /// `ConnectionH2::force_disconnect` directly, so a red here names the
15147 /// query under test, where the fall-through route arrives through a write
15148 /// pass and a state machine that can each shift for reasons of their own.
15149 ///
15150 /// TO SEE THIS RED: pass `true` in place of `tls_wants_write` to
15151 /// `force_disconnect_action` in `force_disconnect`'s `Position::Server`
15152 /// arm. Measured: this test fails with `a server holding nothing must
15153 /// close rather than wait for a flush that has nothing to flush, got
15154 /// Continue`, while the sibling above stays green. The mirror constant
15155 /// `false` is measured at exactly ONE failure, and that failure is
15156 /// the sibling above — so neither constant can replace the query, and the
15157 /// sibling is the suite's only witness against `false`.
15158 #[test]
15159 fn force_disconnect_closes_when_no_tls_records_are_pending() {
15160 let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
15161 // Same fixture, zero records: the query answers `false`.
15162 let (mut connection, _peer) = connection_with_backpressure(&pool, 0, 0, H2State::GoAway);
15163
15164 assert!(
15165 !connection.socket.socket_wants_write(),
15166 "premise: nothing buffered, so the table reads its second boolean \
15167 as false"
15168 );
15169 assert!(
15170 !connection.core.peer_gone_after_final_goaway(),
15171 "premise: the peer must still be live, so the close comes from the \
15172 record count and not from the first boolean"
15173 );
15174
15175 let result = connection.force_disconnect();
15176
15177 assert!(
15178 matches!(result, MuxResult::CloseSession),
15179 "a server holding nothing must close rather than wait for a flush \
15180 that has nothing to flush, got {result:?}"
15181 );
15182 }
15183
15184 /// A force-disconnect raised from the READ path still re-arms, because the
15185 /// shell settles it before the pass returns.
15186 ///
15187 /// The sibling above calls `H2Shell::force_disconnect` directly, where the
15188 /// raise and the settlement are two statements of one function. This one
15189 /// enters through `H2Shell::readable`, which is the shape that made the
15190 /// inversion necessary: `ConnectionH2::handle_read` force-disconnects at
15191 /// seven sites, its `pub` signature returns `MuxResult` and cannot carry an
15192 /// `H2ForceDisconnectTarget`, so the core parks one and answers
15193 /// provisionally. Nothing the core does afterwards writes `readiness`; only
15194 /// `H2Shell::settled` does, and only if the shell remembers to call it.
15195 ///
15196 /// `Mux` reads `Readiness::filter_interest` between entry points to decide
15197 /// whether a connection gets a `writable` at all, so a settlement deferred
15198 /// to the next entry point is not the same as one performed here: the
15199 /// connection would be left with records in rustls and no WRITABLE
15200 /// interest to come back on.
15201 ///
15202 /// TO SEE THIS RED: in `H2Shell::readable`, return `result` instead of
15203 /// `self.settled(result)`. Measured: this test fails on its interest
15204 /// assertion, `a force-disconnect raised inside handle_read must still
15205 /// re-arm WRITABLE interest before the read pass returns`, `left: Hup`,
15206 /// `right: Writable | Error | Hup`, while
15207 /// `force_disconnect_re_arms_while_tls_records_are_pending` stays green —
15208 /// the direct call is not the read path, which is why both exist.
15209 #[test]
15210 fn a_force_disconnect_raised_inside_handle_read_is_settled_before_the_pass_returns() {
15211 use std::io::Write;
15212
15213 let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
15214 // A kernel that accepts nothing, so the records outlive any flush and
15215 // `force_disconnect_action` takes its `ReArmAndContinue` row.
15216 let (mut connection, mut peer) =
15217 connection_with_backpressure(&pool, 2, 0, H2State::ClientPreface);
15218 let mut context = test_context(&pool);
15219 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
15220
15221 assert!(
15222 connection.socket.socket_wants_write(),
15223 "premise: this harness must report buffered TLS records, or the \
15224 settlement takes the closing row and writes no interest at all"
15225 );
15226 assert!(
15227 !connection.core.peer_gone_after_final_goaway(),
15228 "premise: the peer must still be live, or the decision \
15229 short-circuits to CloseSession on its first boolean and never \
15230 reads the record count"
15231 );
15232
15233 // A preface whose very first octet is wrong: `handle_read`'s early
15234 // guard refuses it and reaches `force_disconnect` without the
15235 // connection ever leaving `H2State::ClientPreface` on its own.
15236 let mut wire = Vec::with_capacity(CLIENT_PREFACE_SIZE);
15237 wire.extend_from_slice(b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n");
15238 wire.extend_from_slice(&[0, 0, 0, 4, 0, 0, 0, 0, 0]);
15239 wire[0] ^= 0xff;
15240 peer.write_all(&wire).expect("loopback write must complete");
15241 peer.flush().expect("loopback flush must complete");
15242
15243 // Withdraw WRITABLE interest first. The fixture hands
15244 // `WRITABLE | HUP | ERROR` to the constructor, so asserting on it
15245 // untouched would read the fixture's own argument back and would hold
15246 // even if nothing settled. Starting from `HUP` alone makes the
15247 // settlement the only thing that can restore the bit.
15248 connection.core.readiness.interest = Ready::HUP;
15249 assert!(
15250 !connection.core.readiness.event.is_writable(),
15251 "premise: the WRITABLE event must start clear, so the signal below \
15252 is the only thing that can set it, got {:?}",
15253 connection.core.readiness
15254 );
15255
15256 let mut result = MuxResult::Continue;
15257 for _ in 0..64 {
15258 result = connection.readable(&mut context, EndpointClient(&mut router));
15259 if matches!(connection.core.state, H2State::Error) {
15260 break;
15261 }
15262 std::thread::yield_now();
15263 }
15264
15265 assert!(
15266 matches!(connection.core.state, H2State::Error),
15267 "premise: the read pass must have reached force_disconnect, which \
15268 assigns H2State::Error, got {:?}",
15269 connection.core.state
15270 );
15271 assert!(
15272 matches!(result, MuxResult::Continue),
15273 "a server still holding TLS records must not close: CloseSession \
15274 sends FIN and destroys them, got {result:?}"
15275 );
15276 assert_eq!(
15277 connection.core.readiness.interest,
15278 Ready::WRITABLE | Ready::HUP | Ready::ERROR,
15279 "a force-disconnect raised inside handle_read must still re-arm \
15280 WRITABLE interest before the read pass returns, got {:?}",
15281 connection.core.readiness
15282 );
15283 assert!(
15284 connection.core.readiness.event.is_writable(),
15285 "the settlement must signal the edge-triggered WRITABLE event, or \
15286 the event loop never revisits this connection, got {:?}",
15287 connection.core.readiness
15288 );
15289 assert!(
15290 connection.core.poll_force_disconnect().is_none(),
15291 "the settlement must have TAKEN the parked target, so a second \
15292 settlement cannot run"
15293 );
15294 }
15295
15296 /// Build a bare server-side `ConnectionH2` for tests that only exercise
15297 /// connection-level bookkeeping. The socket is never read or written.
15298 ///
15299 /// Returns the accepted peer alongside it; keep it alive for the duration
15300 /// of the test so the socket stays connected and reads answer `WouldBlock`
15301 /// instead of `ECONNREFUSED`.
15302 fn test_h2_connection(
15303 pool: &Rc<RefCell<Pool>>,
15304 graceful_shutdown_deadline: Option<Duration>,
15305 ) -> (H2Shell<mio::net::TcpStream>, std::net::TcpStream) {
15306 let (socket, peer) = connected_socket();
15307 let connection = H2Shell::new(
15308 Ulid::generate(),
15309 socket,
15310 Position::Server,
15311 &mut PoolBufferSource::new(Rc::downgrade(pool)),
15312 H2FloodConfig::default(),
15313 H2ConnectionConfig::default(),
15314 Duration::from_secs(30),
15315 graceful_shutdown_deadline,
15316 Duration::from_secs(30),
15317 Some((H2StreamId::Zero, CLIENT_PREFACE_SIZE)),
15318 Ready::READABLE | Ready::HUP | Ready::ERROR,
15319 )
15320 .expect("a pool with free buffers must yield an H2 connection");
15321 (connection, peer)
15322 }
15323
15324 /// `snapshot_rtts` samples the frontend once per connection, when the
15325 /// first stream of the connection is logged, and hands every later stream
15326 /// the same value, in the same pass or in a later one (issues #1339 Q11
15327 /// and #1590).
15328 ///
15329 /// The first two calls stand for two streams recycling inside one pass:
15330 /// all five production callers of this method sit on a recycle or reset
15331 /// path, and more than one of them can run in a single `write_streams`
15332 /// sweep. The third, through a fresh `ShellEndpoint`, stands for a stream
15333 /// logged in a later pass. The counter is
15334 /// [`super::super::tcp_info_reads`], bumped by every
15335 /// `getsockopt(TCP_INFO)` the mux issues.
15336 ///
15337 /// The premise is that the sample is `Some`: the connection sits on a live
15338 /// loopback socket, so `TCP_INFO` answers. Without it a `local_rtt` that
15339 /// had lost the socket would pass the equality assertions while reporting
15340 /// nothing at all.
15341 ///
15342 /// TO SEE THIS RED: in `memoized_rtt` (`lib/src/protocol/mux/mod.rs`),
15343 /// delete the early `return sample;` of the memo hit. Every call then
15344 /// reads the socket and the second assertion fails with ``a second stream
15345 /// finishing in the SAME pass must reuse the connection sample, not read
15346 /// TCP_INFO again``, `left: 2`, `right: 1`.
15347 #[test]
15348 fn snapshot_rtts_samples_the_frontend_once_per_connection() {
15349 let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
15350 let (connection, _peer) = test_h2_connection(&pool, None);
15351 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
15352 let mut inner = EndpointClient(&mut router);
15353
15354 let before = super::super::tcp_info_reads();
15355 let endpoint = ShellEndpoint::new(&mut inner, &connection.socket, &connection.local_rtt);
15356 let first = connection
15357 .core
15358 .snapshot_rtts(&endpoint, Some(mio::Token(1)));
15359 assert!(
15360 first.0.is_some(),
15361 "premise: TCP_INFO must answer on a live loopback frontend"
15362 );
15363 let second = connection
15364 .core
15365 .snapshot_rtts(&endpoint, Some(mio::Token(2)));
15366 assert_eq!(
15367 super::super::tcp_info_reads() - before,
15368 1,
15369 "a second stream finishing in the SAME pass must reuse the \
15370 connection sample, not read TCP_INFO again"
15371 );
15372 assert_eq!(
15373 second.0, first.0,
15374 "every stream finishing in one pass reports one client_rtt — the \
15375 identity #1339 Q11 traded the per-recycle syscall for"
15376 );
15377 assert_eq!(
15378 (first.1, second.1),
15379 (None, None),
15380 "the router holds no backend, so server_rtt stays None and costs \
15381 no read"
15382 );
15383
15384 let endpoint = ShellEndpoint::new(&mut inner, &connection.socket, &connection.local_rtt);
15385 let next_pass = connection.core.snapshot_rtts(&endpoint, None);
15386 assert_eq!(
15387 super::super::tcp_info_reads() - before,
15388 1,
15389 "a stream logged in a later pass must reuse the connection's one \
15390 sample, not read TCP_INFO again"
15391 );
15392 assert_eq!(
15393 next_pass.0, first.0,
15394 "every stream of one connection reports that connection's one \
15395 client_rtt"
15396 );
15397 }
15398
15399 /// A pass that logs no stream reads no `TCP_INFO`, and a backend H2
15400 /// connection never reads one for the access log (issue #1590).
15401 ///
15402 /// TO SEE THIS RED: make `ShellEndpoint::new` sample eagerly — call
15403 /// `super::sample_rtt(socket.socket_ref())` and store it in `sample` —
15404 /// which is the shape `Mux::refresh_client_rtt` had. The first assertion
15405 /// fails with ``a pass that logs no stream must not read TCP_INFO``,
15406 /// `left: 1`, `right: 0`.
15407 #[test]
15408 fn a_pass_that_logs_nothing_reads_no_tcp_info() {
15409 let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
15410 let (mut connection, _peer) = test_h2_connection(&pool, None);
15411 let mut context = test_context(&pool);
15412 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
15413
15414 let before = super::super::tcp_info_reads();
15415 connection.cancel_timed_out_streams(&mut context, &mut EndpointClient(&mut router));
15416 assert_eq!(
15417 super::super::tcp_info_reads() - before,
15418 0,
15419 "a pass that logs no stream must not read TCP_INFO"
15420 );
15421 assert_eq!(connection.local_rtt.get(), None);
15422
15423 connection.core.position = Position::Client(
15424 "rtt-cluster".into(),
15425 super::super::BackendId {
15426 slot: super::super::BackendSlot(0),
15427 backend_id: Rc::from("rtt-backend"),
15428 address: "127.0.0.1:1".parse().expect("a literal socket address"),
15429 metric_incarnation: Default::default(),
15430 },
15431 BackendStatus::Connected,
15432 );
15433 let mut inner = EndpointClient(&mut router);
15434 let endpoint = ShellEndpoint::new(&mut inner, &connection.socket, &connection.local_rtt);
15435 assert_eq!(
15436 connection
15437 .core
15438 .snapshot_rtts(&endpoint, Some(mio::Token(1))),
15439 (None, None),
15440 "a backend H2 connection must not report RTTs onto the shared stream"
15441 );
15442 assert_eq!(
15443 super::super::tcp_info_reads() - before,
15444 0,
15445 "a backend H2 connection must not read TCP_INFO for the access log"
15446 );
15447 }
15448
15449 /// Two streams reaped in one pass: two access-log lines, one frontend
15450 /// `TCP_INFO` read between them, and the same `client_rtt` on both lines.
15451 ///
15452 /// Drives the real log path — `H2Shell::cancel_timed_out_streams` through
15453 /// `ShellEndpoint`, `snapshot_rtts` and `Stream::generate_access_log` —
15454 /// and reads the lines the logger wrote, so it pins what an operator sees:
15455 /// the `client_rtt` cell is present (not `-`) and coherent across the
15456 /// pass, and `server_rtt` is `-` for streams that never reached a backend.
15457 ///
15458 /// TO SEE THIS RED: in `ConnectionH2::snapshot_rtts`, return `None` in
15459 /// place of `endpoint.local_rtt()`. Both lines then carry `-` in the
15460 /// `client_rtt` cell and the presence assertion fails.
15461 #[test]
15462 fn streams_reaped_in_one_pass_log_one_present_client_rtt() {
15463 let reads = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
15464 let reads_in_pass = reads.clone();
15465 let output = crate::capture_test_logs(move || {
15466 let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 8, 16384)));
15467 let (mut connection, _peer) = test_h2_connection(&pool, None);
15468 let mut context = test_context(&pool);
15469 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
15470
15471 let armed_at = connection.core.now;
15472 for stream_id in [1, 3] {
15473 let gid = context
15474 .create_stream(Ulid::generate(), 1 << 16)
15475 .expect("test context must create a stream");
15476 connection
15477 .core
15478 .stream_table
15479 .register(stream_id, gid, armed_at);
15480 }
15481 context.now = armed_at + connection.core.stream_idle_timeout + Duration::from_millis(1);
15482
15483 let before = super::super::tcp_info_reads();
15484 connection.cancel_timed_out_streams(&mut context, &mut EndpointClient(&mut router));
15485 reads_in_pass.store(
15486 super::super::tcp_info_reads() - before,
15487 std::sync::atomic::Ordering::Relaxed,
15488 );
15489 });
15490
15491 let cells: Vec<(&str, &str)> = output
15492 .lines()
15493 .filter_map(|line| {
15494 line.split_whitespace().find_map(|token| {
15495 let cells: Vec<&str> = token.split('/').collect();
15496 (cells.len() == 5).then(|| (cells[3], cells[4]))
15497 })
15498 })
15499 .collect();
15500 assert_eq!(
15501 cells.len(),
15502 2,
15503 "two reaped streams must write two access-log lines, got: {output}"
15504 );
15505 for (client_rtt, server_rtt) in &cells {
15506 assert_ne!(
15507 *client_rtt, "-",
15508 "the access log's client_rtt must be present, got: {output}"
15509 );
15510 assert_eq!(
15511 *server_rtt, "-",
15512 "a stream that never reached a backend has no server_rtt, got: {output}"
15513 );
15514 }
15515 assert_eq!(
15516 cells[0].0, cells[1].0,
15517 "streams logged in one pass share one client_rtt, got: {output}"
15518 );
15519 assert_eq!(
15520 reads.load(std::sync::atomic::Ordering::Relaxed),
15521 1,
15522 "two streams logged in one pass must cost one frontend TCP_INFO \
15523 read and no backend read"
15524 );
15525 }
15526
15527 /// The graceful-shutdown budget is armed from the `now` handed to
15528 /// `graceful_goaway` and evaluated against `ConnectionH2::now` — not
15529 /// against the real clock at either end.
15530 ///
15531 /// `armed_at` is deliberately 100 s in the future relative to the
15532 /// connection's construction snapshot. That gap is what makes the test
15533 /// discriminating: a `graceful_goaway` that armed `started_at` from
15534 /// `Instant::now()` would put it ~100 s in the PAST relative to
15535 /// `armed_at`, and the "not yet elapsed" assertion would trip.
15536 ///
15537 /// To SEE THIS RED: restore either half of the old behaviour.
15538 /// (a) `self.started_at = Some(Instant::now());` in
15539 /// `h2_drain::H2DrainState::begin_graceful_drain` — the `assert_eq!`
15540 /// on `drain.__test_started_at()` fails: ``assertion `left == right`
15541 /// failed: graceful_goaway must arm the budget from its `now`
15542 /// parameter / left: Some(Instant { tv_sec: 107343, .. }) / right:
15543 /// Some(Instant { tv_sec: 107443, .. })`` — the two differing by
15544 /// exactly the 100 s offset above.
15545 /// (b) `started_at.elapsed() >= deadline` in
15546 /// `h2_drain::H2DrainState::deadline_elapsed` — the LAST assertion
15547 /// fails with `advancing only the connection snapshot past the
15548 /// budget must force close`, because no real time passes in this
15549 /// test, so the forced-close budget never expires however far the
15550 /// connection's clock is moved.
15551 #[test]
15552 fn graceful_shutdown_deadline_is_evaluated_against_the_connection_snapshot() {
15553 let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
15554 let deadline = Duration::from_secs(5);
15555 let (mut connection, _peer) = test_h2_connection(&pool, Some(deadline));
15556
15557 // Nothing armed yet: `started_at` is None, so the budget cannot expire.
15558 assert!(
15559 !connection.core.graceful_shutdown_deadline_elapsed(),
15560 "an unarmed drain must never report its budget as elapsed"
15561 );
15562
15563 let armed_at = connection.core.now + Duration::from_secs(100);
15564 connection.graceful_goaway(armed_at);
15565 assert_eq!(
15566 connection.core.drain.__test_started_at(),
15567 Some(armed_at),
15568 "graceful_goaway must arm the budget from its `now` parameter"
15569 );
15570
15571 // One millisecond short of the budget, measured on the connection's
15572 // clock. Real elapsed time here is microseconds.
15573 connection.core.now = armed_at + deadline - Duration::from_millis(1);
15574 assert!(
15575 !connection.core.graceful_shutdown_deadline_elapsed(),
15576 "the budget must not expire before `started_at + deadline` on the \
15577 connection's own clock"
15578 );
15579
15580 connection.core.now = armed_at + deadline;
15581 assert!(
15582 connection.core.graceful_shutdown_deadline_elapsed(),
15583 "advancing only the connection snapshot past the budget must force close"
15584 );
15585 }
15586
15587 /// The RFC 9113 §5.1.2 back-pressure window is driven by
15588 /// `ConnectionH2::now`, so a refusal burst is weighed against the window
15589 /// that was open when the pass started — it cannot decay mid-pass.
15590 ///
15591 /// To SEE THIS RED: restore
15592 /// `if self.refuse_window_start.elapsed() >= BACKPRESSURE_WINDOW_DURATION`
15593 /// in `record_refusal_for_backpressure`. The window then never rolls over
15594 /// (no real time passes) and the 50th refusal lands in the same window as
15595 /// the first 49, so the `refuse_count_window` assertion fails first:
15596 /// ``assertion `left == right` failed: a refusal in a new window must
15597 /// restart the count, not top up the old one / left: 1000 / right: 1``. The
15598 /// two assertions after it would also fail — `mcs_backpressure_applied`
15599 /// becomes true and `settings_max_concurrent_streams` is halved off a burst
15600 /// that actually spanned two windows.
15601 #[test]
15602 fn backpressure_window_rolls_over_on_the_connection_snapshot() {
15603 let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
15604 let (mut connection, _peer) = test_h2_connection(&pool, None);
15605 let advertised = connection
15606 .core
15607 .local_settings
15608 .settings_max_concurrent_streams;
15609
15610 // One short of the burst threshold, all inside the first window.
15611 for _ in 0..BACKPRESSURE_REFUSAL_THRESHOLD - 1 {
15612 connection.core.record_refusal_for_backpressure();
15613 }
15614 assert_eq!(
15615 connection.core.refuse_count_window,
15616 BACKPRESSURE_REFUSAL_THRESHOLD - 1
15617 );
15618 assert!(
15619 !connection.core.mcs_backpressure_applied,
15620 "back-pressure must not apply below the refusal threshold"
15621 );
15622
15623 // Cross into the next window on the connection's clock alone.
15624 connection.core.now += BACKPRESSURE_WINDOW_DURATION;
15625 connection.core.record_refusal_for_backpressure();
15626
15627 assert_eq!(
15628 connection.core.refuse_count_window, 1,
15629 "a refusal in a new window must restart the count, not top up the old one"
15630 );
15631 assert!(
15632 !connection.core.mcs_backpressure_applied,
15633 "two refusals spread across two windows are not a burst"
15634 );
15635 assert_eq!(
15636 connection
15637 .core
15638 .local_settings
15639 .settings_max_concurrent_streams,
15640 advertised,
15641 "MAX_CONCURRENT_STREAMS must be untouched when no burst occurred"
15642 );
15643 }
15644
15645 /// The RFC 9113 §6.5 SETTINGS-ACK deadline is evaluated against
15646 /// `ConnectionH2::now`. `flush_pending_control_frames` is the `writable`
15647 /// half of the pair; the `poll_read_target` half at `h2.rs` shares the predicate.
15648 ///
15649 /// To SEE THIS RED: restore `sent_at.elapsed() >= SETTINGS_ACK_TIMEOUT` in
15650 /// `flush_pending_control_frames`. No real time passes in this test, so the
15651 /// deadline never fires however far the connection's clock is advanced, and
15652 /// the third assertion fails with `advancing only the connection snapshot
15653 /// to the deadline must emit GOAWAY`.
15654 #[test]
15655 fn settings_ack_deadline_is_evaluated_against_the_connection_snapshot() {
15656 let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
15657 let (mut connection, _peer) = test_h2_connection(&pool, None);
15658
15659 let sent_at = connection.core.now;
15660 connection.core.settings_sent_at = Some(sent_at);
15661
15662 // One millisecond short of the budget on the connection's clock.
15663 connection.core.now = sent_at + SETTINGS_ACK_TIMEOUT - Duration::from_millis(1);
15664 assert!(
15665 matches!(
15666 connection.core.flush_pending_control_frames(),
15667 H2ControlFlushTarget::Proceed
15668 ),
15669 "the SETTINGS-ACK deadline must not fire before SETTINGS_ACK_TIMEOUT \
15670 on the connection's own clock"
15671 );
15672 assert_eq!(
15673 connection.core.settings_sent_at,
15674 Some(sent_at),
15675 "a deadline that has not fired must leave the ACK timer armed"
15676 );
15677
15678 // At the budget: GOAWAY(SETTINGS_TIMEOUT).
15679 connection.core.now = sent_at + SETTINGS_ACK_TIMEOUT;
15680 assert!(
15681 matches!(
15682 connection.core.flush_pending_control_frames(),
15683 H2ControlFlushTarget::Done(_)
15684 ),
15685 "advancing only the connection snapshot to the deadline must emit GOAWAY"
15686 );
15687 assert_eq!(
15688 connection.core.settings_sent_at, None,
15689 "goaway must disarm the ACK timer so the timeout cannot re-fire"
15690 );
15691 }
15692
15693 /// The per-stream liveness guard reaps against `ConnectionH2::now`, and
15694 /// `cancel_timed_out_streams` adopts `context.now` on entry — this pins
15695 /// both the entry-point mirror and the deadline arithmetic in
15696 /// `H2StreamTable::collect_timed_out`.
15697 ///
15698 /// To SEE THIS RED: restore `let now = Instant::now();` in
15699 /// `cancel_timed_out_streams` (in place of `let now = self.now;`). The
15700 /// injected instant is then ignored, no stream is ever old enough to reap
15701 /// because no real time passes, and the reap assertion fails with
15702 /// `advancing only the mux snapshot past the idle deadline must reap the
15703 /// stream`. Deleting the `self.now = context.now;` entry-point assignment
15704 /// turns the same assertion red, with the same message, for the same reason.
15705 #[test]
15706 fn per_stream_liveness_reaping_is_evaluated_against_the_connection_snapshot() {
15707 let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
15708 let (mut connection, _peer) = test_h2_connection(&pool, None);
15709 let mut context = test_context(&pool);
15710 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
15711
15712 let idle_timeout = connection.core.stream_idle_timeout;
15713 let armed_at = connection.core.now;
15714 let gid = context
15715 .create_stream(Ulid::generate(), 1 << 16)
15716 .expect("test context must create a stream");
15717 connection.core.stream_table.register(1, gid, armed_at);
15718
15719 // Exactly at the deadline: the predicate is a strict `>`, so the stream
15720 // survives.
15721 context.now = armed_at + idle_timeout;
15722 connection.cancel_timed_out_streams(&mut context, &mut EndpointClient(&mut router));
15723 assert!(
15724 connection.core.control_tx.pending().is_empty(),
15725 "a stream exactly at its idle deadline must not be reaped"
15726 );
15727
15728 // One millisecond past it: reaped with RST_STREAM(CANCEL).
15729 context.now = armed_at + idle_timeout + Duration::from_millis(1);
15730 connection.cancel_timed_out_streams(&mut context, &mut EndpointClient(&mut router));
15731 assert!(
15732 !connection.core.control_tx.pending().is_empty(),
15733 "advancing only the mux snapshot past the idle deadline must reap the stream"
15734 );
15735 assert_eq!(
15736 connection.core.control_tx.pending()[0],
15737 (1, H2Error::Cancel),
15738 "the reaper must queue RST_STREAM(CANCEL) for the timed-out stream"
15739 );
15740 }
15741
15742 /// A mass idle-timeout reap is the only path that queues more than one
15743 /// RST_STREAM between two `flush_pending_control_frames` passes:
15744 /// `cancel_timed_out_streams` walks the whole timed-out set and calls
15745 /// `enqueue_rst` for every entry inside ONE pass. The cap therefore has to
15746 /// live at the insert, not at the drain — a drain-side check bounds what
15747 /// is *written*, and says nothing about how large the queue got before
15748 /// anyone looked. See sozu-proxy/sozu#1413.
15749 ///
15750 /// `h2_control_tx::pending_rst_bound` keeps the bound above four resets
15751 /// per advertised concurrent stream, so a real sweep cannot reach it on
15752 /// its own; the queue still holds what every caller queued since the last
15753 /// successful drain. This test registers more wire ids than the bound on
15754 /// the stream table directly, exactly as its sibling
15755 /// `per_stream_liveness_reaping_is_evaluated_against_the_connection_snapshot`
15756 /// does, because the accept path is not what is under test here.
15757 ///
15758 /// `check_invariants` is asserted explicitly because the reaper is also
15759 /// reachable from `Mux::timeout` against a fully silent peer — the very
15760 /// scenario that produces a mass reap — where `handle_frame`, the only
15761 /// production caller of `check_invariants`, never runs at all. The reaper
15762 /// is not the only insert path outside that caller: the
15763 /// DATA-on-closed-stream reset sits in `handle_header_state`, which
15764 /// `handle_read` returns into without ever reaching `handle_frame`. That one
15765 /// is rate-limited by `record_glitch` + `check_flood_or_return!` and
15766 /// cannot by itself fill the queue — see `ConnectionH2::enqueue_rst`.
15767 ///
15768 /// To SEE THIS RED: delete the `pending_before >= self.max_pending` guard
15769 /// at the top of `H2ControlTx::enqueue_rst` (`h2_control_tx.rs`), then run
15770 /// `cargo test -p sozu-lib --locked mass_reap` (one positional filter —
15771 /// cargo rejects a second one). Every reaped stream is then queued
15772 /// unconditionally and `H2ControlTx::check_invariants` — the post-condition
15773 /// every mutating method on that type runs — panics inside the sweep with
15774 /// `pending RST queue must stay within its hard cap (escalates at the
15775 /// cap)`. That clause moved to the module with this commit and is NOT
15776 /// restated in `ConnectionH2::check_invariants`, so the explicit call
15777 /// below no longer covers the queue bound — the assertion at the end of
15778 /// this test is what pins it from the connection's side.
15779 #[test]
15780 fn mass_reap_keeps_the_pending_rst_queue_within_its_hard_cap() {
15781 // `Stream::new` checks out two buffers per stream and the connection
15782 // itself holds one for its zero buffer, so the ceiling has to clear
15783 // `2 * REAPED` with room to spare or `create_stream` hands back
15784 // `None` — which is how the first draft of this test failed.
15785 //
15786 // A production bound (`h2_control_tx::pending_rst_bound`, at least
15787 // `MIN_PENDING_RST_STREAMS`) would need thousands of streams and two
15788 // pool buffers each; what is under test is that the queue honours
15789 // its instance's bound, so the test installs a small one.
15790 let bound = 200;
15791 let reaped = bound + 32;
15792 let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 2 * reaped + 4, 16384)));
15793 let (mut connection, _peer) = test_h2_connection(&pool, None);
15794 connection.core.control_tx = h2_control_tx::H2ControlTx::with_cap(bound);
15795 let mut context = test_context(&pool);
15796 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
15797
15798 let idle_timeout = connection.core.stream_idle_timeout;
15799 let armed_at = connection.core.now;
15800 for i in 0..reaped {
15801 let gid = context
15802 .create_stream(Ulid::generate(), 1 << 16)
15803 .expect("test context must create a stream");
15804 // RFC 9113 §5.1.1: client-initiated stream ids are odd.
15805 connection
15806 .core
15807 .stream_table
15808 .register(2 * i as StreamId + 1, gid, armed_at);
15809 }
15810
15811 // One sweep, every stream past its deadline: the entire set is reaped
15812 // and RST-queued with no intervening flush.
15813 context.now = armed_at + idle_timeout + Duration::from_millis(1);
15814 connection.cancel_timed_out_streams(&mut context, &mut EndpointClient(&mut router));
15815
15816 #[cfg(debug_assertions)]
15817 connection.core.check_invariants(&context);
15818 assert!(
15819 connection.core.control_tx.pending().len() <= bound,
15820 "a mass reap must stop queueing at the cap, got {} entries",
15821 connection.core.control_tx.pending().len()
15822 );
15823 }
15824
15825 // ── flood accounting: refusals, provoked resets, the denominator ────
15826
15827 /// A peer that keeps opening streams Sōzu refuses neither grows the
15828 /// connection's state nor escapes the flood detector: a refused stream
15829 /// was never registered, so nothing ever removes its id from `rst_sent`,
15830 /// and each refusal past the limit counts as a glitch.
15831 ///
15832 /// The refusals here fall in the band back-pressure opens, local limit
15833 /// lowered below the advertised one, which cost nothing before.
15834 #[test]
15835 fn refused_streams_keep_rst_sent_bounded_and_trip_the_glitch_budget() {
15836 use std::io::Write;
15837
15838 const BATCHES: u32 = 50;
15839 const PER_BATCH: u32 = 1000;
15840 let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 8, 16384)));
15841 let (mut connection, mut peer) = test_h2_connection(&pool, None);
15842 let mut context = test_context(&pool);
15843 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
15844 connection.core.state = H2State::Header;
15845 connection
15846 .core
15847 .stream_table
15848 .set_expect_read(Some((H2StreamId::Zero, 9)));
15849 connection
15850 .core
15851 .local_settings
15852 .settings_max_concurrent_streams = 0;
15853 // `:method GET`, `:path /`, `:scheme https`, then `:authority`
15854 // as a literal without indexing: no HPACK table state to track.
15855 let mut block = vec![0x82, 0x84, 0x87, 0x01, 0x09];
15856 block.extend_from_slice(b"localhost");
15857
15858 let mut next_id: u32 = 1;
15859 'batches: for _ in 0..BATCHES {
15860 let mut wire = Vec::new();
15861 for _ in 0..PER_BATCH {
15862 wire.extend(orphan_frame(
15863 1,
15864 parser::FLAG_END_HEADERS | parser::FLAG_END_STREAM,
15865 next_id,
15866 block.len() as u32,
15867 &block,
15868 ));
15869 next_id += 2;
15870 }
15871 peer.write_all(&wire).expect("loopback write must complete");
15872 peer.flush().expect("loopback flush must complete");
15873 // One read pass handles one frame header or one payload: drive
15874 // until the batch's last stream id has been seen.
15875 let last_id = next_id - 2;
15876 for _ in 0..(16 * PER_BATCH) {
15877 connection.core.readiness.event.insert(Ready::READABLE);
15878 connection.core.readiness.interest.insert(Ready::READABLE);
15879 connection.readable(&mut context, EndpointClient(&mut router));
15880 let _ = connection.core.flush_pending_control_frames();
15881 connection.core.clear_output();
15882 if matches!(connection.core.state, H2State::GoAway | H2State::Error) {
15883 break 'batches;
15884 }
15885 if connection.core.stream_table.highest_peer_stream_id() >= last_id
15886 && matches!(connection.core.state, H2State::Header)
15887 {
15888 break;
15889 }
15890 }
15891 }
15892
15893 assert!(
15894 connection.core.stream_table.rst_sent_mut().len() < 100,
15895 "refused stream ids must not accumulate in rst_sent, got {}",
15896 connection.core.stream_table.rst_sent_mut().len()
15897 );
15898 assert!(
15899 matches!(connection.core.state, H2State::GoAway | H2State::Error),
15900 "endless refusals must trip the flood detector, got {:?} after {} streams",
15901 connection.core.state,
15902 next_id / 2
15903 );
15904 }
15905
15906 /// Every reset the peer provokes is a protocol error on its part and
15907 /// counts as a glitch, so provoked resets have a per-window rate limit
15908 /// independent of how many streams the connection served.
15909 #[test]
15910 fn peer_provoked_resets_count_as_glitches() {
15911 let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
15912 let (mut connection, _peer) = test_h2_connection(&pool, None);
15913 let now = connection.core.now;
15914 for _ in 0..1_000_000 {
15915 connection.core.flood_detector.record_stream_opened();
15916 }
15917 let glitch_budget = H2FloodConfig::default().max_glitch_count();
15918 let mut tripped_at = None;
15919 for n in 1..=(glitch_budget + 1) {
15920 connection.core.now = now;
15921 if connection
15922 .core
15923 .account_emitted_rst(H2Error::ProtocolError, RstOrigin::PeerProvoked)
15924 .is_some()
15925 {
15926 tripped_at = Some(n);
15927 break;
15928 }
15929 }
15930 assert_eq!(
15931 tripped_at,
15932 Some(glitch_budget + 1),
15933 "the provoked reset past the glitch budget must end the connection"
15934 );
15935 }
15936
15937 /// Streams the peer opens count toward the RST caps' denominator only
15938 /// once a backend answers them: a stream Sōzu refuses, answers itself or
15939 /// sees reset before a response cannot dilute the caps.
15940 #[test]
15941 fn unanswered_streams_do_not_dilute_the_reset_caps() {
15942 const FLOOR: u64 = 10;
15943 let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 70, 16384)));
15944 let (mut connection, _peer) = test_h2_connection(&pool, None);
15945 let mut context = test_context(&pool);
15946 connection.core.flood_detector = h2_flood_detector::H2FloodDetector::new(
15947 H2FloodConfig::from_optional(
15948 None,
15949 None,
15950 None,
15951 None,
15952 None,
15953 None,
15954 None,
15955 None,
15956 Some(FLOOR),
15957 None,
15958 None,
15959 None,
15960 None,
15961 None,
15962 ),
15963 connection.core.now,
15964 );
15965 for i in 0..30u32 {
15966 connection
15967 .core
15968 .create_stream(2 * i + 1, &mut context)
15969 .expect("the pool must hand out stream buffers");
15970 }
15971 let mut tripped_at = None;
15972 for n in 1..=(FLOOR + 1) {
15973 if connection
15974 .core
15975 .flood_detector
15976 .record_rst_received(false, false, connection.core.now)
15977 .is_some()
15978 {
15979 tripped_at = Some(n);
15980 break;
15981 }
15982 }
15983 assert_eq!(
15984 tripped_at,
15985 Some(FLOOR + 1),
15986 "thirty unanswered streams must not raise the pre-response cap"
15987 );
15988 }
15989
15990 /// Resets Sōzu emits on its own initiative — here the idle reaper's
15991 /// `RST_STREAM(CANCEL)` — are not charged to the peer: neither the
15992 /// CVE-2025-8671 MadeYouReset emitted-RST counter nor the pending RST
15993 /// queue's bound may accumulate them across drains.
15994 ///
15995 /// Two sweeps of 150 reaped streams with a control flush between them:
15996 /// 300 local resets in the connection's lifetime, never more than 150
15997 /// pending at once.
15998 ///
15999 /// TO SEE THIS RED: in `ConnectionH2::cancel_timed_out_streams`, pass
16000 /// `RstOrigin::PeerProvoked` instead of `RstOrigin::Local` to
16001 /// `ConnectionH2::enqueue_rst`. The emitted-RST counter then reads 300.
16002 #[test]
16003 fn local_resets_are_not_charged_to_the_peer() {
16004 const PER_SWEEP: usize = 150;
16005 let pool = Rc::new(RefCell::new(Pool::with_capacity(
16006 2,
16007 4 * PER_SWEEP + 4,
16008 16384,
16009 )));
16010 let (mut connection, _peer) = test_h2_connection(&pool, None);
16011 let mut context = test_context(&pool);
16012 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
16013 let idle_timeout = connection.core.stream_idle_timeout;
16014
16015 for sweep in 0..2 {
16016 let armed_at = connection.core.now;
16017 for i in 0..PER_SWEEP {
16018 let gid = context
16019 .create_stream(Ulid::generate(), 1 << 16)
16020 .expect("test context must create a stream");
16021 let wire_id = 2 * (sweep * PER_SWEEP + i) as StreamId + 1;
16022 connection
16023 .core
16024 .stream_table
16025 .register(wire_id, gid, armed_at);
16026 }
16027 context.now = armed_at + idle_timeout + Duration::from_millis(1);
16028 connection.cancel_timed_out_streams(&mut context, &mut EndpointClient(&mut router));
16029 // The SETTINGS-ACK watchdog is not under test.
16030 connection.core.settings_sent_at = None;
16031 let flushed = connection.core.flush_pending_control_frames();
16032 assert!(
16033 !matches!(flushed, H2ControlFlushTarget::Done(_)),
16034 "sweep {sweep}: local resets must not end the connection"
16035 );
16036 assert!(
16037 !matches!(connection.core.state, H2State::GoAway | H2State::Error),
16038 "sweep {sweep}: local resets must not send GOAWAY"
16039 );
16040 }
16041 assert_eq!(
16042 connection
16043 .core
16044 .flood_detector
16045 .total_rst_streams_emitted_lifetime(),
16046 0,
16047 "reaper CANCELs are Sōzu's decision, not the peer's"
16048 );
16049 }
16050
16051 // ── forcefully_terminate_answer arms WRITABLE for ET epoll ───────────
16052 //
16053 // Gap A in the h2spec diagnosis: the pre-fix code set `interest` but
16054 // never raised `event`, so `filter_interest() = event & interest` was
16055 // zero and `writable()` was never scheduled. This test pins the fix.
16056
16057 #[test]
16058 fn test_forcefully_terminate_answer_arms_event_and_interest() {
16059 let pool = make_pool_for_invariant_16();
16060 let ulid = Ulid::generate();
16061 let mut stream = make_stream_for_invariant_16(&pool, ulid);
16062 let mut readiness = Readiness::new();
16063
16064 assert!(!readiness.interest.is_writable());
16065 assert!(!readiness.event.is_writable());
16066
16067 forcefully_terminate_answer(&mut stream, &mut readiness, H2Error::ProtocolError);
16068
16069 assert!(
16070 readiness.interest.is_writable(),
16071 "forcefully_terminate_answer must set the WRITABLE interest bit"
16072 );
16073 assert!(
16074 readiness.event.is_writable(),
16075 "forcefully_terminate_answer must set the WRITABLE event bit — \
16076 without this, filter_interest() = 0 under edge-triggered epoll \
16077 and writable() is never scheduled (h2spec Gap A)"
16078 );
16079 }
16080
16081 // ── peer-address snapshot in the MUX-H2 log envelope ─────────────────
16082 //
16083 // `log_context!` / `log_context_stream!` render `peer=` from
16084 // `SocketHandler::peer_addr`, which is a snapshot the handler cached, not a
16085 // live `getpeername(2)`. Both tests below make the two answers differ on
16086 // purpose: the socket is genuinely connected to a loopback listener, so the
16087 // live lookup succeeds and would print `127.0.0.1:<port>`; only a macro
16088 // reading the snapshot can print `10.0.0.42:12345`.
16089 //
16090 // That gap is the PROXY-protocol symptom in miniature — the frontend's real
16091 // peer is the load balancer while the cache holds the advertised client —
16092 // and the same read is what keeps the slot populated after the peer's RST,
16093 // when `getpeername(2)` answers ENOTCONN and the live lookup collapses to
16094 // `None` on exactly the error lines an operator is reading.
16095
16096 /// A live, established loopback connection plus the address
16097 /// `getpeername(2)` reports for it. The listener is returned so the
16098 /// connection stays up for the whole test: these tests assert the snapshot
16099 /// wins even while the live lookup is perfectly healthy, so a half-dead
16100 /// socket would weaken them rather than strengthen them.
16101 fn connected_loopback_stream() -> (
16102 std::net::TcpListener,
16103 mio::net::TcpStream,
16104 std::net::SocketAddr,
16105 ) {
16106 let listener = std::net::TcpListener::bind("127.0.0.1:0")
16107 .expect("test listener must bind to a loopback port");
16108 let live_peer = listener
16109 .local_addr()
16110 .expect("test listener must report its local address");
16111 let stream =
16112 std::net::TcpStream::connect(live_peer).expect("loopback connect must complete");
16113 stream
16114 .set_nonblocking(true)
16115 .expect("mio requires a nonblocking stream");
16116 (listener, mio::net::TcpStream::from_std(stream), live_peer)
16117 }
16118
16119 /// Address the handler caches. Deliberately non-loopback so it cannot
16120 /// collide with whatever ephemeral port the live lookup would report.
16121 const CACHED_PEER: &str = "10.0.0.42:12345";
16122
16123 /// Build a server-side H2 connection whose frontend handler caches
16124 /// [`CACHED_PEER`] while its socket is really connected to `live_peer`.
16125 fn connection_with_cached_peer(
16126 pool: &Rc<RefCell<Pool>>,
16127 stream: mio::net::TcpStream,
16128 ) -> H2Shell<crate::socket::SessionTcpStream> {
16129 let session_ulid = Ulid::generate();
16130 let socket = crate::socket::SessionTcpStream::new(
16131 stream,
16132 session_ulid,
16133 Some(
16134 CACHED_PEER
16135 .parse()
16136 .expect("the cached peer literal must parse"),
16137 ),
16138 );
16139 H2Shell::new(
16140 session_ulid,
16141 socket,
16142 Position::Server,
16143 &mut PoolBufferSource::new(Rc::downgrade(pool)),
16144 H2FloodConfig::default(),
16145 H2ConnectionConfig::default(),
16146 Duration::from_secs(30),
16147 None,
16148 Duration::from_secs(30),
16149 Some((H2StreamId::Zero, CLIENT_PREFACE_SIZE)),
16150 Ready::READABLE | Ready::HUP | Ready::ERROR,
16151 )
16152 .expect("a pool with free buffers must yield an H2 connection")
16153 }
16154
16155 /// To SEE THIS RED: in `log_context!` (h2.rs), put
16156 /// `peer = $self.socket.socket_ref().peer_addr().ok(),` back in place of
16157 /// `peer = $self.peer_address,`. The rendered line then carries the
16158 /// loopback address the socket is really connected to, so the first
16159 /// assertion fails on the missing cached address.
16160 ///
16161 /// Substituting the pre-snapshot `peer = $self.socket.peer_addr(),` does
16162 /// NOT turn this red, and that is worth stating: for a handler that
16163 /// already caches, reading the cache and reading a snapshot taken from
16164 /// that cache agree by construction. What the snapshot changed is WHICH
16165 /// object is read on a log line, not what comes back — so the call-count
16166 /// property, not this one, is what distinguishes them. It is pinned by
16167 /// [`log_context_reads_the_peer_address_once_per_connection`].
16168 #[test]
16169 fn log_context_renders_the_cached_peer_not_a_live_lookup() {
16170 let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
16171 let (_listener, stream, live_peer) = connected_loopback_stream();
16172 let connection = connection_with_cached_peer(&pool, stream);
16173
16174 // Premise of the test: the live lookup is healthy and disagrees with the
16175 // cache. Without this the assertions below could pass for the wrong
16176 // reason (both answers happening to be the same address).
16177 assert_eq!(
16178 connection.socket.socket_ref().peer_addr().ok(),
16179 Some(live_peer),
16180 "the test socket must be genuinely connected, so a live lookup succeeds"
16181 );
16182
16183 let rendered = log_context!(connection.core);
16184
16185 assert!(
16186 rendered.contains(&format!("peer=Some({CACHED_PEER})")),
16187 "MUX-H2 context must render the cached peer: {rendered}"
16188 );
16189 assert!(
16190 !rendered.contains(&live_peer.to_string()),
16191 "MUX-H2 context must not fall back to the live getpeername(2) answer: {rendered}"
16192 );
16193 }
16194
16195 /// To SEE THIS RED: in `log_context_stream!` (h2.rs), put
16196 /// `peer = $self.socket.socket_ref().peer_addr().ok(),` back in place of
16197 /// `peer = $self.peer_address,`. The per-stream envelope then
16198 /// diverges from the connection envelope, which is worse than either being
16199 /// wrong alone: the same session renders two different peers depending on
16200 /// whether a stream happened to be in scope at the callsite.
16201 #[test]
16202 fn log_context_stream_renders_the_cached_peer_not_a_live_lookup() {
16203 let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 20, 16_384)));
16204 let (_listener, stream, live_peer) = connected_loopback_stream();
16205 let connection = connection_with_cached_peer(&pool, stream);
16206 let mux_stream = make_stream_for_invariant_16(&pool, connection.core.session_ulid);
16207 let http_context = &mux_stream.context;
16208
16209 assert_eq!(
16210 connection.socket.socket_ref().peer_addr().ok(),
16211 Some(live_peer),
16212 "the test socket must be genuinely connected, so a live lookup succeeds"
16213 );
16214
16215 let rendered = log_context_stream!(connection.core, http_context);
16216
16217 assert!(
16218 rendered.contains(&format!("peer=Some({CACHED_PEER})")),
16219 "per-stream MUX-H2 context must render the cached peer: {rendered}"
16220 );
16221 assert!(
16222 !rendered.contains(&live_peer.to_string()),
16223 "per-stream MUX-H2 context must not fall back to the live \
16224 getpeername(2) answer: {rendered}"
16225 );
16226 }
16227
16228 /// A [`SocketHandler`] that counts how often it is asked for the peer
16229 /// address, delegating every other method to a real loopback stream.
16230 ///
16231 /// The two tests above pin WHICH address the log prefix renders. Neither
16232 /// can see how many times the connection reaches into the socket to get
16233 /// it, because both production handlers answer from a cache and so give
16234 /// the same answer however often they are asked. This handler makes the
16235 /// count observable.
16236 struct PeerAddrCountingSocket {
16237 stream: mio::net::TcpStream,
16238 peer: std::net::SocketAddr,
16239 calls: Rc<std::cell::Cell<usize>>,
16240 }
16241
16242 impl SocketHandler for PeerAddrCountingSocket {
16243 fn socket_read(&mut self, buf: &mut [u8]) -> (usize, SocketResult) {
16244 self.stream.socket_read(buf)
16245 }
16246
16247 fn socket_write(&mut self, buf: &[u8]) -> (usize, SocketResult) {
16248 self.stream.socket_write(buf)
16249 }
16250
16251 fn socket_write_vectored(&mut self, bufs: &[IoSlice]) -> (usize, SocketResult) {
16252 self.stream.socket_write_vectored(bufs)
16253 }
16254
16255 fn socket_ref(&self) -> &mio::net::TcpStream {
16256 &self.stream
16257 }
16258
16259 fn socket_mut(&mut self) -> &mut mio::net::TcpStream {
16260 &mut self.stream
16261 }
16262
16263 fn peer_addr(&self) -> Option<std::net::SocketAddr> {
16264 self.calls.set(self.calls.get() + 1);
16265 Some(self.peer)
16266 }
16267
16268 fn protocol(&self) -> crate::socket::TransportProtocol {
16269 crate::socket::TransportProtocol::Tcp
16270 }
16271
16272 fn read_error(&self) {}
16273
16274 fn write_error(&self) {}
16275 }
16276
16277 /// The `peer=` slot is read from the socket exactly ONCE per connection —
16278 /// at construction — however many log lines the connection renders.
16279 ///
16280 /// This is the property that makes `peer_address` worth having. It is not
16281 /// about which address appears (the two tests above own that); it is about
16282 /// `log_context!` no longer being a socket operation. The macro expands at
16283 /// 113 production callsites in this file, so before the snapshot every
16284 /// method that logged was `Front`-coupled whether or not it did any I/O —
16285 /// which is 113 of the ~140 total `Front` touch points in `h2.rs`.
16286 ///
16287 /// TO SEE THIS RED: in `log_context!` (h2.rs), put
16288 /// `peer = $self.socket.peer_addr(),` back in place of
16289 /// `peer = $self.peer_address,`. The count then rises by one per rendered
16290 /// line and the final assertion fails with
16291 /// `the peer address must be read once, at construction, not once per log line:
16292 /// left: 4, right: 1`.
16293 #[test]
16294 fn log_context_reads_the_peer_address_once_per_connection() {
16295 let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
16296 let (_listener, stream, _live_peer) = connected_loopback_stream();
16297 let calls = Rc::new(std::cell::Cell::new(0usize));
16298 let session_ulid = Ulid::generate();
16299 let socket = PeerAddrCountingSocket {
16300 stream,
16301 peer: CACHED_PEER
16302 .parse()
16303 .expect("the cached peer literal must parse"),
16304 calls: Rc::clone(&calls),
16305 };
16306 let connection = H2Shell::new(
16307 session_ulid,
16308 socket,
16309 Position::Server,
16310 &mut PoolBufferSource::new(Rc::downgrade(&pool)),
16311 H2FloodConfig::default(),
16312 H2ConnectionConfig::default(),
16313 Duration::from_secs(30),
16314 None,
16315 Duration::from_secs(30),
16316 Some((H2StreamId::Zero, CLIENT_PREFACE_SIZE)),
16317 Ready::READABLE | Ready::HUP | Ready::ERROR,
16318 )
16319 .expect("a pool with free buffers must yield an H2 connection");
16320
16321 assert_eq!(
16322 calls.get(),
16323 1,
16324 "construction takes exactly one peer_addr() snapshot"
16325 );
16326
16327 // Premise: the rendered lines really do carry the address, so a zero
16328 // count below would mean the slot went missing rather than that it
16329 // became free.
16330 for _ in 0..3 {
16331 let rendered = log_context!(connection.core);
16332 assert!(
16333 rendered.contains(&format!("peer=Some({CACHED_PEER})")),
16334 "each rendered line must still carry the peer: {rendered}"
16335 );
16336 }
16337
16338 assert_eq!(
16339 calls.get(),
16340 1,
16341 "the peer address must be read once, at construction, not once per log line"
16342 );
16343 }
16344
16345 // ── The read-side two-call protocol (poll_read_target / handle_read) ──
16346
16347 /// `read_space` hands out the buffer the core named, and hands out the
16348 /// stream's READ buffer — the one `Stream::split` labels `rbuffer` for the
16349 /// connection's position — not its write buffer.
16350 ///
16351 /// This is the only genuinely new logic in the read-side inversion: every
16352 /// other line moved. Both halves are pinned because the two failure modes
16353 /// differ — offering the wrong *kawa* corrupts the peer's request while
16354 /// the response is parsed as H2 payload, offering the wrong *stream*
16355 /// writes one stream's DATA into another's.
16356 ///
16357 /// To SEE THIS RED: in `read_buffer` (h2.rs), return
16358 /// `streams[global_stream_id].split(position).wbuffer` in place of
16359 /// `.rbuffer`.
16360 #[test]
16361 fn read_space_offers_the_read_buffer_of_the_stream_the_core_named() {
16362 let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 20, 16_384)));
16363 let (mut connection, _peer) = test_h2_connection(&pool, None);
16364 let mut context = test_context(&pool);
16365
16366 let gid = context
16367 .create_stream(Ulid::generate(), 1 << 16)
16368 .expect("test context must create a stream");
16369
16370 // Premise: the two per-stream buffers really are distinct allocations,
16371 // so a pointer comparison below can tell them apart at all.
16372 let front = context.streams[gid].front.storage.space().as_ptr();
16373 let back = context.streams[gid].back.storage.space().as_ptr();
16374 let zero = connection.core.zero.storage.space().as_ptr();
16375 assert_ne!(
16376 front, back,
16377 "a stream's request and response buffers must be distinct allocations"
16378 );
16379
16380 let offered = read_space(
16381 &mut connection.core.zero,
16382 &mut context.streams,
16383 &connection.core.position,
16384 H2StreamId::Zero,
16385 9,
16386 );
16387 assert_eq!(
16388 offered.as_ptr(),
16389 zero,
16390 "H2StreamId::Zero must offer the connection-level scratch buffer"
16391 );
16392 assert_eq!(
16393 offered.len(),
16394 9,
16395 "the offer is capped at the byte debt, never the whole free space"
16396 );
16397
16398 let offered = read_space(
16399 &mut connection.core.zero,
16400 &mut context.streams,
16401 &connection.core.position,
16402 H2StreamId::Other { id: 1, gid },
16403 7,
16404 );
16405 assert_eq!(
16406 offered.as_ptr(),
16407 front,
16408 "a server position must offer the stream's request (front) buffer"
16409 );
16410 assert_ne!(
16411 offered.as_ptr(),
16412 back,
16413 "the offered buffer must not be the stream's response (back) buffer"
16414 );
16415 assert_eq!(
16416 offered.len(),
16417 7,
16418 "the offer is capped at the byte debt, never the whole free space"
16419 );
16420 }
16421
16422 /// A frame whose payload is zero bytes long owes the socket nothing, and
16423 /// the core says so with its own variant rather than an `amount` of 0.
16424 ///
16425 /// That distinction is load-bearing, not cosmetic. A caller handed
16426 /// `Fill { amount: 0 }` would issue a zero-length read, and
16427 /// `update_readiness_after_read(0, SocketResult::Continue, ..)` returns
16428 /// `true` for it — "nothing arrived, stop" — so `handle_read` would return
16429 /// before the frame state machine ever ran, and every empty SETTINGS,
16430 /// empty DATA and SETTINGS ACK would stop being parsed.
16431 ///
16432 /// To SEE THIS RED: in `poll_read_target` (h2.rs), replace the trailing
16433 /// `if amount > 0 { .. } else { .. }` with its `Fill` half alone —
16434 /// keep the `available_space` guard and end the function with
16435 /// `H2ReadTarget::Fill { stream_id, amount }`, dropping the
16436 /// `set_expect_read(None)` / `H2ReadTarget::Skip(stream_id)` branch.
16437 #[test]
16438 fn poll_read_target_skips_the_read_when_the_frame_carries_no_payload() {
16439 let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
16440 let (mut connection, _peer) = test_h2_connection(&pool, None);
16441 let mut context = test_context(&pool);
16442 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
16443
16444 connection.core.state = H2State::Header;
16445 connection
16446 .core
16447 .stream_table
16448 .set_expect_read(Some((H2StreamId::Zero, 0)));
16449
16450 let target = connection
16451 .core
16452 .poll_read_target(&mut context, &mut EndpointClient(&mut router));
16453 assert!(
16454 matches!(target, H2ReadTarget::Skip(H2StreamId::Zero)),
16455 "a zero-length payload must be a Skip, not a zero-length Fill: {target:?}"
16456 );
16457 assert!(
16458 connection.core.stream_table.expect_read().is_none(),
16459 "the settled byte debt must be cleared before the frame is dispatched"
16460 );
16461 }
16462
16463 /// The core is told how many bytes arrived, and it subtracts exactly that
16464 /// many from the debt it offered — a short read leaves the remainder owed,
16465 /// it does not restart the frame.
16466 ///
16467 /// 6 is neither of the two numbers the test hands in, so this cannot pass
16468 /// by echoing the fixture: `amount` is 9 and `size` is 3.
16469 ///
16470 /// To SEE THIS RED: in `handle_read` (h2.rs), pass `amount` in place of
16471 /// `amount - size` to `set_expect_read`.
16472 #[test]
16473 fn handle_read_subtracts_the_byte_count_the_caller_reported() {
16474 let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
16475 let (mut connection, _peer) = test_h2_connection(&pool, None);
16476 let mut context = test_context(&pool);
16477 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
16478
16479 // Not ClientPreface: a short read there runs the early-preface check
16480 // instead, which is a different branch with its own coverage.
16481 connection.core.state = H2State::Header;
16482 connection
16483 .core
16484 .stream_table
16485 .set_expect_read(Some((H2StreamId::Zero, 9)));
16486
16487 let result = connection.core.handle_read(
16488 &mut context,
16489 EndpointClient(&mut router),
16490 H2StreamId::Zero,
16491 H2ReadOutcome::Filled {
16492 amount: 9,
16493 size: 3,
16494 status: SocketResult::Continue,
16495 },
16496 );
16497
16498 assert!(
16499 matches!(result, MuxResult::Continue),
16500 "a short read keeps the connection running: {result:?}"
16501 );
16502 assert_eq!(
16503 connection.core.stream_table.expect_read(),
16504 Some((H2StreamId::Zero, 6)),
16505 "a 3-byte answer to a 9-byte offer must leave 6 bytes owed"
16506 );
16507 assert_eq!(
16508 connection.core.zero.storage.data().len(),
16509 3,
16510 "the core must consume exactly the bytes the caller reported"
16511 );
16512 }
16513
16514 /// A read that returned nothing clears the READABLE **event** and leaves
16515 /// the READABLE **interest** alone, so the connection is re-armed for the
16516 /// next epoll wake-up instead of being taken off the loop.
16517 ///
16518 /// The interest half is asserted on purpose: `signal_pending_write` and
16519 /// this path both touch `Readiness.event` only, and an assertion written
16520 /// against `interest` cannot see either of them move.
16521 ///
16522 /// To SEE THIS RED: in `handle_read` (h2.rs), call
16523 /// `update_readiness_after_write` in place of `update_readiness_after_read`
16524 /// — the WRITABLE bit is cleared instead and the READABLE event survives.
16525 #[test]
16526 fn handle_read_clears_the_readable_event_not_the_interest_when_nothing_arrived() {
16527 let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
16528 let (mut connection, _peer) = test_h2_connection(&pool, None);
16529 let mut context = test_context(&pool);
16530 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
16531
16532 connection.core.state = H2State::Header;
16533 connection
16534 .core
16535 .stream_table
16536 .set_expect_read(Some((H2StreamId::Zero, 9)));
16537 connection.core.readiness.event = Ready::READABLE;
16538 connection.core.readiness.interest = Ready::READABLE | Ready::HUP | Ready::ERROR;
16539
16540 let result = connection.core.handle_read(
16541 &mut context,
16542 EndpointClient(&mut router),
16543 H2StreamId::Zero,
16544 H2ReadOutcome::Filled {
16545 amount: 9,
16546 size: 0,
16547 status: SocketResult::WouldBlock,
16548 },
16549 );
16550
16551 assert!(
16552 matches!(result, MuxResult::Continue),
16553 "an empty read is not a close: {result:?}"
16554 );
16555 assert!(
16556 !connection.core.readiness.event.is_readable(),
16557 "an empty read must clear the READABLE event"
16558 );
16559 assert!(
16560 connection.core.readiness.interest.is_readable(),
16561 "an empty read must NOT drop the READABLE interest"
16562 );
16563 assert_eq!(
16564 connection.core.stream_table.expect_read(),
16565 Some((H2StreamId::Zero, 9)),
16566 "an empty read settles no part of the byte debt"
16567 );
16568 }
16569
16570 // ── RFC 9113 §4.3: a refused stream must not desynchronise HPACK ──────
16571 //
16572 // Field compression state is scoped to the whole connection, not to a
16573 // stream. When `handle_header_state` refuses a stream — during a
16574 // graceful drain, over MAX_CONCURRENT_STREAMS, or on buffer-pool
16575 // exhaustion — the HEADERS payload it drops is an HPACK field block the
16576 // peer's encoder has *already* applied to its own dynamic table. Dropping
16577 // it without decoding leaves our decoder permanently behind the peer's
16578 // encoder, and every later header block on the surviving connection then
16579 // resolves the wrong dynamic entry or fails outright.
16580
16581 /// To SEE THIS RED: in the `(H2State::Discard, _)` arm of
16582 /// [`ConnectionH2::handle_read`], remove the `if let Some(discarded) =
16583 /// self.discarded_field_block.take() && ...` block that calls
16584 /// [`decode_discarded_field_block`], restoring the unconditional
16585 /// `kawa.storage.clear()`. The peer's second block is then a one-byte
16586 /// reference to dynamic index 62 that our decoder has never been told
16587 /// about, and the decode fails with an out-of-table index. Verified
16588 /// 2026-09-21 (see this commit's message for the exact failure output).
16589 #[test]
16590 fn a_refused_stream_keeps_the_hpack_decoder_in_sync() {
16591 use std::io::Write;
16592
16593 let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 20, 16_384)));
16594 let (mut connection, mut peer) = test_h2_connection(&pool, None);
16595 let mut context = test_context(&pool);
16596 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
16597
16598 // Past the preface/SETTINGS handshake, waiting on a frame header, and
16599 // draining — so the next client-initiated stream is refused rather
16600 // than created. Drain is only the cheapest of the three refusal
16601 // triggers; MAX_CONCURRENT_STREAMS and pool exhaustion reach the same
16602 // `refuse_stream_and_discard` call.
16603 connection.core.state = H2State::Header;
16604 connection
16605 .core
16606 .stream_table
16607 .set_expect_read(Some((H2StreamId::Zero, 9)));
16608 connection.core.drain.__test_set_draining();
16609
16610 // The peer's encoder. It indexes a header whose *name* is in
16611 // neither table, so this block appends `x-sozu-probe: alpha` to the
16612 // peer's dynamic table at index 62.
16613 let mut peer_encoder = crate::protocol::mux::hpack::Encoder::new();
16614 let refused_block = peer_encoder.encode([
16615 (&b":method"[..], &b"GET"[..]),
16616 (&b":scheme"[..], &b"https"[..]),
16617 (&b":authority"[..], &b"example.com"[..]),
16618 (&b":path"[..], &b"/refused"[..]),
16619 (&b"x-sozu-probe"[..], &b"alpha"[..]),
16620 ]);
16621 assert!(
16622 refused_block.len() < 16_384,
16623 "the probe block must fit one HEADERS frame under the default max frame size"
16624 );
16625
16626 // HEADERS, END_STREAM | END_HEADERS, stream 1.
16627 let mut frame = Vec::with_capacity(9 + refused_block.len());
16628 frame.extend_from_slice(&(refused_block.len() as u32).to_be_bytes()[1..]);
16629 frame.push(1);
16630 frame.push(parser::FLAG_END_STREAM | parser::FLAG_END_HEADERS);
16631 frame.extend_from_slice(&1u32.to_be_bytes());
16632 frame.extend_from_slice(&refused_block);
16633 peer.write_all(&frame)
16634 .expect("loopback write must complete");
16635 peer.flush().expect("loopback flush must complete");
16636
16637 // Two passes consume the 9-byte header and then the payload; the rest
16638 // are `WouldBlock` no-ops. Bounded so a delivery hiccup fails loudly
16639 // instead of hanging.
16640 for _ in 0..64 {
16641 connection.readable(&mut context, EndpointClient(&mut router));
16642 if matches!(connection.core.state, H2State::Header)
16643 && matches!(
16644 connection.core.stream_table.expect_read(),
16645 Some((H2StreamId::Zero, 9))
16646 )
16647 && !connection.core.control_tx.pending().is_empty()
16648 {
16649 break;
16650 }
16651 std::thread::yield_now();
16652 }
16653
16654 // Premise of the test: we really went through the refusal path, the
16655 // stream was refused rather than created, and the connection survived.
16656 assert_eq!(
16657 connection.core.control_tx.pending(),
16658 vec![(1, H2Error::RefusedStream)],
16659 "the drain gate must refuse stream 1 with RST_STREAM(REFUSED_STREAM)"
16660 );
16661 assert!(
16662 connection.core.stream_table.is_empty(),
16663 "a refused stream must not be created"
16664 );
16665 assert!(
16666 !matches!(connection.core.state, H2State::Error | H2State::GoAway),
16667 "refusing one stream must leave the connection usable: {:?}",
16668 connection.core.state
16669 );
16670
16671 // The peer's next block references the dynamic entry its encoder added
16672 // while encoding the refused request — a single indexed field.
16673 let next_block = peer_encoder.encode([(&b"x-sozu-probe"[..], &b"alpha"[..])]);
16674 assert_eq!(
16675 next_block.len(),
16676 1,
16677 "the peer must now reference its dynamic entry, not re-send a literal"
16678 );
16679
16680 let mut decoded = Vec::new();
16681 let status = connection
16682 .core
16683 .hpack
16684 .decoder_mut()
16685 .decode_with_cb(&next_block, |k, v| {
16686 decoded.push((k.into_owned(), v.into_owned()));
16687 });
16688
16689 assert!(
16690 status.is_ok(),
16691 "the connection decoder must still resolve the peer's dynamic table \
16692 after a refused stream, got {status:?}"
16693 );
16694 assert_eq!(
16695 decoded,
16696 vec![(b"x-sozu-probe".to_vec(), b"alpha".to_vec())],
16697 "the refused stream's field block must have been decoded into the \
16698 connection's HPACK context"
16699 );
16700 }
16701
16702 /// Serialise one frame header followed by `payload`.
16703 fn orphan_frame(
16704 frame_type: u8,
16705 flags: u8,
16706 stream_id: u32,
16707 len: u32,
16708 payload: &[u8],
16709 ) -> Vec<u8> {
16710 let mut frame = Vec::with_capacity(9 + payload.len());
16711 frame.extend_from_slice(&len.to_be_bytes()[1..]);
16712 frame.push(frame_type);
16713 frame.push(flags);
16714 frame.extend_from_slice(&stream_id.to_be_bytes());
16715 frame.extend_from_slice(payload);
16716 frame
16717 }
16718
16719 /// A PING the peer sends after the orphaned payload: once its ACK sits in
16720 /// `zero`, the connection has provably read past that payload.
16721 fn orphan_ping() -> Vec<u8> {
16722 orphan_frame(6, 0, 0, 8, b"pingpong")
16723 }
16724
16725 /// Open stream 1 with a request whose body follows, then send a DATA
16726 /// header announcing `payload_len` bytes and the first `sent` of them,
16727 /// and drive `readable()` until the connection waits on that payload.
16728 /// Returns what `expect_read` then names.
16729 fn open_stream_mid_data_payload(
16730 connection: &mut H2Shell<mio::net::TcpStream>,
16731 peer: &mut std::net::TcpStream,
16732 context: &mut Context<crate::protocol::mux::test_support::TestListener>,
16733 router: &mut Router,
16734 payload_len: u32,
16735 sent: usize,
16736 ) -> Option<(H2StreamId, usize)> {
16737 use std::io::Write;
16738
16739 connection.core.state = H2State::Header;
16740 connection
16741 .core
16742 .stream_table
16743 .set_expect_read(Some((H2StreamId::Zero, 9)));
16744 let block = crate::protocol::mux::hpack::Encoder::new().encode([
16745 (&b":method"[..], &b"POST"[..]),
16746 (&b":scheme"[..], &b"https"[..]),
16747 (&b":authority"[..], &b"example.com"[..]),
16748 (&b":path"[..], &b"/upload"[..]),
16749 ]);
16750 let mut bytes = orphan_frame(1, parser::FLAG_END_HEADERS, 1, block.len() as u32, &block);
16751 bytes.extend(orphan_frame(0, 0, 1, payload_len, &vec![b'x'; sent]));
16752 peer.write_all(&bytes)
16753 .expect("loopback write must complete");
16754 peer.flush().expect("loopback flush must complete");
16755 for _ in 0..64 {
16756 connection.core.readiness.event.insert(Ready::READABLE);
16757 connection.readable(context, EndpointClient(router));
16758 if let Some((H2StreamId::Other { .. }, remaining)) =
16759 connection.core.stream_table.expect_read()
16760 && (remaining == payload_len as usize - sent
16761 || !connection.core.readiness.interest.is_readable())
16762 {
16763 break;
16764 }
16765 std::thread::yield_now();
16766 }
16767 connection.core.stream_table.expect_read()
16768 }
16769
16770 /// Drive `readable()` until `done` holds, re-inserting the READABLE event
16771 /// AND interest before every pass, so a lost edge-triggered event cannot
16772 /// be what stalls the connection. Returns whether `done` held.
16773 fn drive_reads_with_readiness_forced(
16774 connection: &mut H2Shell<mio::net::TcpStream>,
16775 context: &mut Context<crate::protocol::mux::test_support::TestListener>,
16776 router: &mut Router,
16777 done: impl Fn(&H2Shell<mio::net::TcpStream>) -> bool,
16778 ) -> bool {
16779 for _ in 0..256 {
16780 connection.core.readiness.event.insert(Ready::READABLE);
16781 connection.core.readiness.interest.insert(Ready::READABLE);
16782 connection.readable(context, EndpointClient(router));
16783 if done(connection) {
16784 return true;
16785 }
16786 std::thread::yield_now();
16787 }
16788 false
16789 }
16790
16791 /// Regression test for #1597: a stream reaped by the per-stream idle
16792 /// guard while it owns the DATA payload being read must not leave the
16793 /// connection unable to read.
16794 ///
16795 /// `H2StreamTable::remove` nulls an `expect_read` naming the removed
16796 /// stream. Before the fix nothing else changed, so the connection stayed
16797 /// in `H2State::Frame` with nothing to read, and every later `readable()`
16798 /// returned at the `expect_read() == None` early exit — the remaining
16799 /// payload, the PING behind it and every later frame were never read.
16800 ///
16801 /// TO SEE THIS RED: in [`ConnectionH2::remove_dead_stream`], delete the
16802 /// `skip_orphaned_data_payload` call. The test fails with `after the reap
16803 /// the connection must still read the PING behind the orphaned payload;
16804 /// state=Frame(FrameHeader { payload_len: 10, frame_type: Data, flags: 0,
16805 /// stream_id: 1 }) expect_read=None`. Verified 2026-09-26.
16806 #[test]
16807 fn reaping_a_stream_mid_data_payload_keeps_the_connection_reading() {
16808 use std::io::Write;
16809
16810 let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 20, 16_384)));
16811 let (mut connection, mut peer) = test_h2_connection(&pool, None);
16812 let mut context = test_context(&pool);
16813 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
16814
16815 let expect = open_stream_mid_data_payload(
16816 &mut connection,
16817 &mut peer,
16818 &mut context,
16819 &mut router,
16820 10,
16821 4,
16822 );
16823 assert!(
16824 matches!(expect, Some((H2StreamId::Other { .. }, 6))),
16825 "premise: mid DATA payload with 6 bytes left, got {expect:?}"
16826 );
16827
16828 // The peer pauses past `stream_idle_timeout` (30 s in
16829 // `test_h2_connection`), then sends the rest and a PING.
16830 context.now += Duration::from_secs(31);
16831 let mut rest = b"efghij".to_vec();
16832 rest.extend(orphan_ping());
16833 peer.write_all(&rest).expect("loopback write must complete");
16834 peer.flush().expect("loopback flush must complete");
16835
16836 let read_on =
16837 drive_reads_with_readiness_forced(&mut connection, &mut context, &mut router, |c| {
16838 !c.core.output.is_empty()
16839 });
16840 assert!(
16841 read_on,
16842 "after the reap the connection must still read the PING behind the \
16843 orphaned payload; state={:?} expect_read={:?}",
16844 connection.core.state,
16845 connection.core.stream_table.expect_read()
16846 );
16847 assert!(
16848 connection.core.stream_table.is_empty(),
16849 "premise: the idle guard reaped stream 1"
16850 );
16851 }
16852
16853 /// #1597: the orphaned frame never reaches `handle_data_frame`, so its
16854 /// whole wire length must still be credited to the connection receive
16855 /// window (RFC 9113 §6.9), or every reap shrinks the peer's window.
16856 ///
16857 /// TO SEE THIS RED: in [`ConnectionH2::skip_orphaned_data_payload`],
16858 /// delete the `account_received_bytes` block. The test then fails with
16859 /// `the whole orphaned DATA frame must be credited to the connection
16860 /// window`. Verified 2026-09-26.
16861 #[test]
16862 fn a_skipped_orphaned_data_payload_is_credited_to_the_connection_window() {
16863 use std::io::Write;
16864
16865 let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 20, 16_384)));
16866 let (mut connection, mut peer) = test_h2_connection(&pool, None);
16867 let mut context = test_context(&pool);
16868 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
16869
16870 let expect = open_stream_mid_data_payload(
16871 &mut connection,
16872 &mut peer,
16873 &mut context,
16874 &mut router,
16875 10,
16876 4,
16877 );
16878 assert!(matches!(expect, Some((H2StreamId::Other { .. }, 6))));
16879 context.now += Duration::from_secs(31);
16880 let mut rest = b"efghij".to_vec();
16881 rest.extend(orphan_ping());
16882 peer.write_all(&rest).expect("loopback write must complete");
16883 peer.flush().expect("loopback flush must complete");
16884 assert!(drive_reads_with_readiness_forced(
16885 &mut connection,
16886 &mut context,
16887 &mut router,
16888 |c| !c.core.output.is_empty(),
16889 ));
16890
16891 // A threshold of 1 hands back everything accumulated since the last
16892 // connection WINDOW_UPDATE: exactly the orphaned frame's length.
16893 assert_eq!(
16894 connection.core.flow_control.account_received_bytes(0, 1),
16895 Some(10),
16896 "the whole orphaned DATA frame must be credited to the connection window"
16897 );
16898 }
16899
16900 /// #1597: a remainder close to `max_frame_size` is skipped in pieces as
16901 /// small as the room in `zero` allows, never by waiting for room for all
16902 /// of it. With 4 KiB buffers a 16 KiB remainder can never fit at once.
16903 ///
16904 /// TO SEE THIS RED: in [`ConnectionH2::poll_read_target`], replace
16905 /// `amount.min(available)` by `amount`. The connection then parks for
16906 /// good on the full remainder and the test fails with `the orphaned
16907 /// remainder must be skipped in pieces`.
16908 #[test]
16909 fn a_remainder_near_max_frame_size_is_skipped_in_pieces() {
16910 use std::io::Write;
16911
16912 const PAYLOAD_LEN: u32 = 16_384;
16913
16914 let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 20, 4_096)));
16915 let (mut connection, mut peer) = test_h2_connection(&pool, None);
16916 let mut context = test_context(&pool);
16917 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
16918
16919 let expect = open_stream_mid_data_payload(
16920 &mut connection,
16921 &mut peer,
16922 &mut context,
16923 &mut router,
16924 PAYLOAD_LEN,
16925 0,
16926 );
16927 assert!(
16928 matches!(expect, Some((H2StreamId::Other { .. }, n)) if n == PAYLOAD_LEN as usize),
16929 "premise: waiting on the whole DATA payload, got {expect:?}"
16930 );
16931 assert!(
16932 connection.core.zero.storage.capacity() < PAYLOAD_LEN as usize,
16933 "premise: the remainder cannot fit in `zero` at once"
16934 );
16935
16936 context.now += Duration::from_secs(31);
16937 let mut rest = vec![b'x'; PAYLOAD_LEN as usize];
16938 rest.extend(orphan_ping());
16939 peer.write_all(&rest).expect("loopback write must complete");
16940 peer.flush().expect("loopback flush must complete");
16941
16942 let read_on =
16943 drive_reads_with_readiness_forced(&mut connection, &mut context, &mut router, |c| {
16944 !c.core.output.is_empty()
16945 });
16946 assert!(
16947 read_on,
16948 "the orphaned remainder must be skipped in pieces; state={:?} expect_read={:?}",
16949 connection.core.state,
16950 connection.core.stream_table.expect_read()
16951 );
16952 assert!(
16953 matches!(connection.core.state, H2State::Header),
16954 "the connection must be back on frame headers, not in {:?}",
16955 connection.core.state
16956 );
16957 assert_eq!(
16958 connection.core.flow_control.account_received_bytes(0, 1),
16959 Some(PAYLOAD_LEN),
16960 "the whole orphaned DATA frame must be credited to the connection window"
16961 );
16962 }
16963
16964 /// #1597, parked form: a stream parked on buffer pressure (READABLE
16965 /// interest removed until `try_resume_reading` sees room) and then
16966 /// reaped by `Mux::timeout`'s `cancel_timed_out_streams` must resume
16967 /// reading. `try_resume_reading` only resumes a stream that still
16968 /// exists, so nothing else restores the interest.
16969 ///
16970 /// Unlike the other #1597 tests, this one never re-inserts READABLE
16971 /// interest itself: it calls `readable()` only when the mux would.
16972 ///
16973 /// TO SEE THIS RED: in [`ConnectionH2::skip_orphaned_data_payload`],
16974 /// delete `self.readiness.interest.insert(Ready::READABLE);`. The test
16975 /// fails with `a reaped parked stream must not leave the connection
16976 /// unable to read`.
16977 #[test]
16978 fn reaping_a_stream_parked_on_buffer_pressure_resumes_reading() {
16979 use std::io::Write;
16980
16981 const PAYLOAD_LEN: u32 = 16_384;
16982
16983 let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 20, 4_096)));
16984 let (mut connection, mut peer) = test_h2_connection(&pool, None);
16985 let mut context = test_context(&pool);
16986 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
16987
16988 let expect = open_stream_mid_data_payload(
16989 &mut connection,
16990 &mut peer,
16991 &mut context,
16992 &mut router,
16993 PAYLOAD_LEN,
16994 0,
16995 );
16996 assert!(
16997 matches!(expect, Some((H2StreamId::Other { .. }, n)) if n == PAYLOAD_LEN as usize),
16998 "premise: waiting on the whole DATA payload, got {expect:?}"
16999 );
17000 // The next pass finds no room for the frame in the stream buffer.
17001 connection.readable(&mut context, EndpointClient(&mut router));
17002 assert!(
17003 !connection.core.readiness.interest.is_readable(),
17004 "premise: the stream buffer cannot take the frame, so the read parked"
17005 );
17006
17007 let mut rest = vec![b'x'; PAYLOAD_LEN as usize];
17008 rest.extend(orphan_ping());
17009 peer.write_all(&rest).expect("loopback write must complete");
17010 peer.flush().expect("loopback flush must complete");
17011
17012 // `Mux::timeout` reaps the parked stream.
17013 context.now += Duration::from_secs(31);
17014 connection.cancel_timed_out_streams(&mut context, &mut EndpointClient(&mut router));
17015 assert!(
17016 connection.core.stream_table.is_empty(),
17017 "premise: the idle guard reaped stream 1"
17018 );
17019
17020 let mut read_on = false;
17021 for _ in 0..256 {
17022 connection.core.readiness.event.insert(Ready::READABLE);
17023 if connection.core.readiness.filter_interest().is_readable() {
17024 connection.readable(&mut context, EndpointClient(&mut router));
17025 }
17026 if !connection.core.output.is_empty() {
17027 read_on = true;
17028 break;
17029 }
17030 std::thread::yield_now();
17031 }
17032 assert!(
17033 read_on,
17034 "a reaped parked stream must not leave the connection unable to read; \
17035 state={:?} expect_read={:?} interest={:?}",
17036 connection.core.state,
17037 connection.core.stream_table.expect_read(),
17038 connection.core.readiness.interest
17039 );
17040 }
17041
17042 /// #1597, backend form: `end_stream` on a `Position::Client` connection
17043 /// (the client gave up, or the backend timeout fired) while the backend
17044 /// is mid-way through a response DATA frame. The RST_STREAM(CANCEL)
17045 /// `end_stream` queues must reach the backend, ahead of the PING ACK the
17046 /// connection queues once it reads on. Before #1604 the RST was written
17047 /// into `zero`, which `handle_header_state` parses from its first byte,
17048 /// so an unflushed RST there was read back as if the backend had sent it,
17049 /// then cleared, and never sent; the output queue shares nothing with the
17050 /// read side any more.
17051 ///
17052 /// The loop drives the connection the way `Mux` does: `writable()` and
17053 /// `readable()` only when the filtered readiness allows them.
17054 ///
17055 /// TO SEE THIS RED: in the `Position::Client` arm of
17056 /// [`ConnectionH2::end_stream`], make the RST's `if` condition start with
17057 /// `false &&`, so the RST is never queued. The test then fails with `the
17058 /// backend must receive exactly RST_STREAM(CANCEL) for stream 1, then the
17059 /// PING ACK`, `left` being the ACK alone. Verified 2026-09-27.
17060 #[test]
17061 fn end_stream_on_a_backend_mid_data_payload_sends_its_rst_before_reading_on() {
17062 use std::io::{Read, Write};
17063
17064 let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 20, 16_384)));
17065 let (socket, mut peer) = connected_socket();
17066 peer.set_nonblocking(true)
17067 .expect("the loopback peer must switch to non-blocking");
17068 let backend = Rc::new(RefCell::new(crate::backends::Backend::new(
17069 "orphan-backend",
17070 "127.0.0.1:2".parse().expect("backend address must parse"),
17071 None,
17072 None,
17073 None,
17074 )));
17075 let mut registry = crate::protocol::mux::BackendRegistry::default();
17076 let mut connection = H2Shell::new(
17077 Ulid::generate(),
17078 socket,
17079 Position::Client(
17080 "orphan-cluster".into(),
17081 registry.id_for(&backend),
17082 BackendStatus::Connected,
17083 ),
17084 &mut PoolBufferSource::new(Rc::downgrade(&pool)),
17085 H2FloodConfig::default(),
17086 H2ConnectionConfig::default(),
17087 Duration::from_secs(30),
17088 None,
17089 Duration::from_secs(30),
17090 Some((H2StreamId::Zero, 9)),
17091 Ready::READABLE | Ready::HUP | Ready::ERROR,
17092 )
17093 .expect("a pool with free buffers must yield an H2 connection");
17094 connection.core.state = H2State::Header;
17095 let mut context = test_context(&pool);
17096 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
17097 let gid = context
17098 .create_stream(Ulid::generate(), 1 << 16)
17099 .expect("test context must create a stream");
17100 context.streams[gid].state = StreamState::Linked(mio::Token(1));
17101 // The backend is answering, so the request's HEADERS reached it: only
17102 // a stream opened on the wire is reset (sozu-proxy/sozu#1631).
17103 context.streams[gid].front.consumed = true;
17104 connection
17105 .core
17106 .stream_table
17107 .register(1, gid, connection.core.now);
17108
17109 // The backend starts a 10-byte response DATA frame and sends 4 bytes.
17110 peer.write_all(&orphan_frame(0, 0, 1, 10, b"abcd"))
17111 .expect("loopback write must complete");
17112 for _ in 0..64 {
17113 connection.core.readiness.event.insert(Ready::READABLE);
17114 connection.readable(&mut context, EndpointClient(&mut router));
17115 if matches!(
17116 connection.core.stream_table.expect_read(),
17117 Some((H2StreamId::Other { .. }, 6))
17118 ) {
17119 break;
17120 }
17121 std::thread::yield_now();
17122 }
17123 assert!(
17124 matches!(
17125 connection.core.stream_table.expect_read(),
17126 Some((H2StreamId::Other { .. }, 6))
17127 ),
17128 "premise: mid response DATA payload with 6 bytes left, got {:?}",
17129 connection.core.stream_table.expect_read()
17130 );
17131
17132 connection.end_stream(gid, &mut context);
17133
17134 // The backend, not yet aware of the RST, finishes the frame and pings.
17135 let mut rest = b"efghij".to_vec();
17136 rest.extend(orphan_ping());
17137 peer.write_all(&rest).expect("loopback write must complete");
17138
17139 let mut received = Vec::new();
17140 let expected_len = 13 + 17;
17141 for _ in 0..256 {
17142 connection
17143 .core
17144 .readiness
17145 .event
17146 .insert(Ready::READABLE | Ready::WRITABLE);
17147 if connection.core.readiness.filter_interest().is_writable() {
17148 connection.writable(&mut context, EndpointClient(&mut router));
17149 }
17150 if connection.core.readiness.filter_interest().is_readable() {
17151 connection.readable(&mut context, EndpointClient(&mut router));
17152 }
17153 let mut buf = [0u8; 256];
17154 if let Ok(n) = peer.read(&mut buf) {
17155 received.extend_from_slice(&buf[..n]);
17156 }
17157 if received.len() >= expected_len
17158 || matches!(connection.core.state, H2State::GoAway | H2State::Error)
17159 {
17160 break;
17161 }
17162 std::thread::yield_now();
17163 }
17164
17165 let mut expected = orphan_frame(3, 0, 1, 4, &(H2Error::Cancel as u32).to_be_bytes());
17166 expected.extend(orphan_frame(6, parser::FLAG_ACK, 0, 8, b"pingpong"));
17167 assert_eq!(
17168 received,
17169 expected,
17170 "the backend must receive exactly RST_STREAM(CANCEL) for stream 1, then the \
17171 PING ACK; state={:?} expect_read={:?}",
17172 connection.core.state,
17173 connection.core.stream_table.expect_read()
17174 );
17175 }
17176
17177 /// What [`backend_with_a_linked_stream`] builds. Callers bind `_pool`
17178 /// first so it is dropped last, after everything holding its buffers.
17179 struct LinkedBackend {
17180 connection: H2Shell<mio::net::TcpStream>,
17181 /// The backend's end of the socket.
17182 peer: std::net::TcpStream,
17183 context: Context<TestListener>,
17184 router: Router,
17185 /// The linked stream's global id.
17186 gid: GlobalStreamId,
17187 _pool: Rc<RefCell<Pool>>,
17188 }
17189
17190 /// A backend (`Position::Client`) connection in `state` over a loopback
17191 /// socket, with `interest`, and one stream linked to it by
17192 /// [`H2Shell::start_stream`]: stream 1, whose HEADERS no write pass has
17193 /// sent.
17194 fn backend_with_a_linked_stream(
17195 state: H2State,
17196 status: BackendStatus,
17197 interest: Ready,
17198 ) -> LinkedBackend {
17199 let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 20, 16_384)));
17200 let (socket, peer) = connected_socket();
17201 let backend = Rc::new(RefCell::new(crate::backends::Backend::new(
17202 "idle-backend",
17203 "127.0.0.1:2".parse().expect("backend address must parse"),
17204 None,
17205 None,
17206 None,
17207 )));
17208 let mut registry = crate::protocol::mux::BackendRegistry::default();
17209 let expect_read = match state {
17210 H2State::ClientPreface | H2State::ClientSettings => None,
17211 _ => Some((H2StreamId::Zero, 9)),
17212 };
17213 let mut connection = H2Shell::new(
17214 Ulid::generate(),
17215 socket,
17216 Position::Client("idle-cluster".into(), registry.id_for(&backend), status),
17217 &mut PoolBufferSource::new(Rc::downgrade(&pool)),
17218 H2FloodConfig::default(),
17219 H2ConnectionConfig::default(),
17220 Duration::from_secs(30),
17221 None,
17222 Duration::from_secs(30),
17223 expect_read,
17224 interest,
17225 )
17226 .expect("a pool with free buffers must yield an H2 connection");
17227 connection.core.state = state;
17228 let mut context = test_context(&pool);
17229 let router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
17230 let gid = context
17231 .create_stream(Ulid::generate(), 1 << 16)
17232 .expect("test context must create a stream");
17233 assert!(
17234 connection.start_stream(gid, &mut context),
17235 "premise: the backend connection accepts the stream"
17236 );
17237 assert_eq!(
17238 connection.core.stream_table.get(1),
17239 Some(gid),
17240 "premise: the stream got wire id 1"
17241 );
17242 LinkedBackend {
17243 connection,
17244 peer,
17245 context,
17246 router,
17247 gid,
17248 _pool: pool,
17249 }
17250 }
17251
17252 /// Drive a backend connection the way `Mux` does — `writable()` and
17253 /// `readable()` only when the filtered readiness allows them — and answer
17254 /// every byte its peer end received.
17255 fn drive_and_read_backend(
17256 connection: &mut H2Shell<mio::net::TcpStream>,
17257 peer: &mut std::net::TcpStream,
17258 context: &mut Context<TestListener>,
17259 router: &mut Router,
17260 ) -> Vec<u8> {
17261 use std::io::Read;
17262
17263 let mut received = Vec::new();
17264 for _ in 0..64 {
17265 connection
17266 .core
17267 .readiness
17268 .event
17269 .insert(Ready::READABLE | Ready::WRITABLE);
17270 if connection.core.readiness.filter_interest().is_writable() {
17271 connection.writable(context, EndpointClient(router));
17272 }
17273 if connection.core.readiness.filter_interest().is_readable() {
17274 connection.readable(context, EndpointClient(router));
17275 }
17276 let mut buf = [0u8; 256];
17277 if let Ok(n) = peer.read(&mut buf) {
17278 received.extend_from_slice(&buf[..n]);
17279 }
17280 std::thread::yield_now();
17281 }
17282 received
17283 }
17284
17285 /// Queue a GET request on `gid`'s request side, closed with END_STREAM,
17286 /// the way a relayed request reaches a backend's write pass.
17287 fn queue_request(context: &mut Context<TestListener>, gid: GlobalStreamId) {
17288 let kawa = &mut context.streams[gid].front;
17289 kawa.detached.status_line = kawa::StatusLine::Request {
17290 version: kawa::Version::V20,
17291 method: kawa::Store::Static(b"GET"),
17292 uri: kawa::Store::Static(b"/"),
17293 authority: kawa::Store::Static(b"example.com"),
17294 path: kawa::Store::Static(b"/"),
17295 };
17296 kawa.push_block(kawa::Block::StatusLine);
17297 kawa.push_block(kawa::Block::Header(kawa::Pair {
17298 key: kawa::Store::Static(b"x-client"),
17299 val: kawa::Store::Static(b"alice"),
17300 }));
17301 kawa.push_block(kawa::Block::Flags(kawa::Flags {
17302 end_body: false,
17303 end_chunk: false,
17304 end_header: true,
17305 end_stream: true,
17306 }));
17307 kawa.parsing_phase = kawa::ParsingPhase::Terminated;
17308 }
17309
17310 /// Frame-level pin of `handle_data_frame`'s bodiless branches: a backend
17311 /// answers `method` with the HEADERS block `head` (no END_STREAM, so the
17312 /// stream stays linked, RFC 9113 §8.1), then DATA carrying `hello`
17313 /// flagged END_STREAM. Returns the frames the backend reads after the
17314 /// DATA, whether the payload was queued on the response, whether the
17315 /// stream is still tracked, and how many bytes the DATA frame left in the
17316 /// response buffer.
17317 fn bodiless_response_with_data(
17318 method: crate::protocol::kawa_h1::parser::Method,
17319 head: &[u8],
17320 ) -> (Vec<PeerFrame>, bool, bool, usize) {
17321 use std::io::Write;
17322
17323 let LinkedBackend {
17324 _pool,
17325 mut connection,
17326 mut peer,
17327 mut context,
17328 mut router,
17329 gid,
17330 } = backend_with_a_linked_stream(
17331 H2State::Header,
17332 BackendStatus::Connected,
17333 Ready::READABLE | Ready::HUP | Ready::ERROR,
17334 );
17335 queue_request(&mut context, gid);
17336 context.streams[gid].context.method = Some(method);
17337 // Open on the client side, as a stream the router linked; `Link`
17338 // carries no frontend token for `reset_stream` to end.
17339 context.streams[gid].state = StreamState::Link;
17340 let request = drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
17341 assert!(
17342 peer_frames(&request)
17343 .expect("whole frames")
17344 .iter()
17345 .any(|(kind, _, id, _)| *kind == 1 && *id == 1),
17346 "premise: the request went out on stream 1, got {request:?}"
17347 );
17348
17349 peer.write_all(&orphan_frame(1, 0x4, 1, head.len() as u32, head))
17350 .expect("loopback write must complete");
17351 drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
17352 let end_before = context.streams[gid].back.storage.end;
17353 peer.write_all(&orphan_frame(0, 0x1, 1, 5, b"hello"))
17354 .expect("loopback write must complete");
17355 let after = drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
17356 let after = peer_frames(&after).expect("whole frames");
17357 assert!(
17358 matches!(connection.core.state, H2State::Header),
17359 "the shared connection stays up, got {:?}",
17360 connection.core.state
17361 );
17362 let back = &context.streams[gid].back;
17363 let buffer = back.storage.buffer();
17364 let queued = back.blocks.iter().any(|block| {
17365 matches!(block, kawa::Block::Chunk(kawa::Chunk { data })
17366 if !data.data(buffer).is_empty())
17367 });
17368 let tracked = connection.core.stream_table.get(1).is_some();
17369 let retained = back.storage.end.saturating_sub(end_before);
17370 (after, queued, tracked, retained)
17371 }
17372
17373 /// RFC 9110 §15.3.5, §15.4.5, RFC 9113 §8.1.1: DATA carrying a payload
17374 /// on a 204 still linked to its backend stream resets that stream with
17375 /// PROTOCOL_ERROR before the payload is queued, without a GOAWAY. RFC 9110
17376 /// §9.3.2: the payload of a response to HEAD is dropped from the stream
17377 /// buffer and never queued, and its END_STREAM still ends the response,
17378 /// with no frame sent. A 200 to GET forwards the payload.
17379 ///
17380 /// TO SEE THIS RED: make `ConnectionH2::backend_response_content` return
17381 /// `BackendResponseContent::Forwarded` for every response, or keep the
17382 /// discarded payload behind `storage.head` in `handle_data_frame`.
17383 #[test]
17384 fn a_204_response_carrying_data_resets_its_backend_stream_and_a_head_response_discards_it() {
17385 use crate::protocol::kawa_h1::parser::Method;
17386
17387 // :status 204 (static index 9).
17388 let (frames, queued, tracked, _) = bodiless_response_with_data(Method::Get, &[0x89]);
17389 assert_eq!(
17390 frames,
17391 vec![(3, 0, 1, vec![0, 0, 0, 1])],
17392 "a 204 carrying DATA gets RST_STREAM(PROTOCOL_ERROR) alone"
17393 );
17394 assert!(!queued, "the payload of a 204 is never queued");
17395 assert!(!tracked, "the reset stream is retired");
17396
17397 // :status 200 (static index 8), to HEAD.
17398 let (frames, queued, _, retained) = bodiless_response_with_data(Method::Head, &[0x88]);
17399 assert!(
17400 !frames
17401 .iter()
17402 .any(|(kind, _, _, _)| *kind == 3 || *kind == 7),
17403 "a response to HEAD carrying DATA is not reset, got {frames:?}"
17404 );
17405 assert!(!queued, "the payload of a response to HEAD is never queued");
17406 assert_eq!(
17407 retained, 0,
17408 "the payload of a response to HEAD leaves no byte in the buffer"
17409 );
17410
17411 let (frames, queued, _, retained) = bodiless_response_with_data(Method::Get, &[0x88]);
17412 assert!(
17413 !frames
17414 .iter()
17415 .any(|(kind, _, _, _)| *kind == 3 || *kind == 7),
17416 "premise: a 200 to GET is not reset, got {frames:?}"
17417 );
17418 assert!(queued, "premise: the payload of a 200 to GET is queued");
17419 assert_eq!(
17420 retained, 5,
17421 "premise: a forwarded payload stays in the buffer"
17422 );
17423 }
17424
17425 /// RFC 9113 §5.1: after the PROTOCOL_ERROR reset of a 204 carrying DATA,
17426 /// the trailer block the backend had already sent on that stream is
17427 /// ignored, in one HEADERS frame or split over a CONTINUATION frame
17428 /// (RFC 9113 §6.10). The shared backend connection gets no GOAWAY and no
17429 /// further RST_STREAM, and stays up.
17430 ///
17431 /// TO SEE THIS RED: in the `H2State::Discard` arm of
17432 /// `ConnectionH2::handle_read` (`lib/src/protocol/mux/h2.rs`), drop the
17433 /// `pending_discarded_block` store: the CONTINUATION is taken for a
17434 /// standalone frame and answered GOAWAY(PROTOCOL_ERROR):
17435 /// `split=true: ... got [(7, 0, 0, [0, 0, 0, 0, 0, 0, 0, 1])]`, verified
17436 /// 2026-10-02. The split row is also red on `2ce07244`
17437 /// (sozu-proxy/sozu#1775 before sozu-proxy/sozu#1784 was merged), where
17438 /// the unsplit row already passes.
17439 #[test]
17440 fn a_trailer_after_the_reset_of_a_204_carrying_data_is_ignored() {
17441 use std::io::Write;
17442
17443 for split in [false, true] {
17444 let LinkedBackend {
17445 _pool,
17446 mut connection,
17447 mut peer,
17448 mut context,
17449 mut router,
17450 gid,
17451 } = backend_with_a_linked_stream(
17452 H2State::Header,
17453 BackendStatus::Connected,
17454 Ready::READABLE | Ready::HUP | Ready::ERROR,
17455 );
17456 queue_request(&mut context, gid);
17457 context.streams[gid].context.method =
17458 Some(crate::protocol::kawa_h1::parser::Method::Get);
17459 context.streams[gid].state = StreamState::Link;
17460 drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
17461
17462 // :status 204 (static index 9), without END_STREAM, then DATA
17463 // without END_STREAM: the backend means to end with a trailer.
17464 peer.write_all(&orphan_frame(1, 0x4, 1, 1, &[0x89]))
17465 .expect("loopback write must complete");
17466 drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
17467 peer.write_all(&orphan_frame(0, 0x0, 1, 5, b"hello"))
17468 .expect("loopback write must complete");
17469 let reset =
17470 drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
17471 let reset = peer_frames(&reset).expect("whole frames");
17472 assert_eq!(
17473 reset,
17474 vec![(3, 0, 1, vec![0, 0, 0, 1])],
17475 "split={split}: premise: a 204 carrying DATA gets RST_STREAM(PROTOCOL_ERROR) alone"
17476 );
17477
17478 // The trailer block `x-trailer: one` (literal without indexing),
17479 // ending the stream.
17480 let mut trailer = vec![0x00, 9];
17481 trailer.extend_from_slice(b"x-trailer");
17482 trailer.extend_from_slice(&[3]);
17483 trailer.extend_from_slice(b"one");
17484 let wire = if split {
17485 let (first, rest) = trailer.split_at(4);
17486 let mut wire = orphan_frame(1, 0x1, 1, first.len() as u32, first);
17487 wire.extend(orphan_frame(9, 0x4, 1, rest.len() as u32, rest));
17488 wire
17489 } else {
17490 orphan_frame(1, 0x4 | 0x1, 1, trailer.len() as u32, &trailer)
17491 };
17492 peer.write_all(&wire).expect("loopback write must complete");
17493 let after =
17494 drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
17495 let after = peer_frames(&after).expect("whole frames");
17496 assert!(
17497 !after.iter().any(|(kind, _, _, _)| *kind == 7 || *kind == 3),
17498 "split={split}: the trailer on the reset stream is ignored: no GOAWAY, no \
17499 RST_STREAM, got {after:?}"
17500 );
17501 assert!(
17502 matches!(connection.core.state, H2State::Header),
17503 "split={split}: the shared connection stays up, got {:?}",
17504 connection.core.state
17505 );
17506 }
17507 }
17508
17509 /// sozu-proxy/sozu#1631, `Header` row: a backend stream ended before any
17510 /// write pass sent its HEADERS is idle on the backend, and a RST_STREAM
17511 /// for it is a connection error there (RFC 9113 §5.1, §6.4) that takes
17512 /// down every stream of the shared connection. It must be forgotten: no
17513 /// frame at all.
17514 ///
17515 /// TO SEE THIS RED: in the `Position::Client` arm of
17516 /// [`ConnectionH2::end_stream`], replace `opened_on_the_wire` in the RST
17517 /// condition with `true`. The backend then reads `RST_STREAM(1, CANCEL)`:
17518 /// `left: [0, 0, 4, 3, 0, 0, 0, 0, 1, 0, 0, 0, 8], right: []`. Red on
17519 /// `d5161919`, verified 2026-09-28.
17520 #[test]
17521 fn a_backend_stream_ended_before_its_headers_left_is_not_reset() {
17522 let LinkedBackend {
17523 _pool,
17524 mut connection,
17525 mut peer,
17526 mut context,
17527 mut router,
17528 gid,
17529 } = backend_with_a_linked_stream(
17530 H2State::Header,
17531 BackendStatus::Connected,
17532 Ready::READABLE | Ready::HUP | Ready::ERROR,
17533 );
17534
17535 connection.end_stream(gid, &mut context);
17536 let received =
17537 drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
17538
17539 assert_eq!(
17540 received,
17541 Vec::<u8>::new(),
17542 "the backend must receive nothing for a stream it never saw"
17543 );
17544 assert!(
17545 connection.core.stream_table.is_empty()
17546 && !connection.core.stream_table.rst_sent_contains(1),
17547 "the stream is retired without a reset"
17548 );
17549 assert!(
17550 matches!(connection.core.state, H2State::Header),
17551 "the shared connection stays up, got {:?}",
17552 connection.core.state
17553 );
17554 }
17555
17556 /// sozu-proxy/sozu#1631, `ClientPreface` row: the backend connection's
17557 /// TCP connect is still in progress when the stream ends. The preface
17558 /// must be the first bytes the backend reads (RFC 9113 §3.4), with no
17559 /// frame for the stream anywhere.
17560 ///
17561 /// TO SEE THIS RED: same edit as
17562 /// `a_backend_stream_ended_before_its_headers_left_is_not_reset`. The
17563 /// backend then reads the RST before `PRI * HTTP/2.0`: `the connection
17564 /// preface must come first`. Red on `d5161919`, verified 2026-09-28.
17565 #[test]
17566 fn a_backend_stream_ended_before_the_preface_leaves_the_preface_first() {
17567 let LinkedBackend {
17568 _pool,
17569 mut connection,
17570 mut peer,
17571 mut context,
17572 mut router,
17573 gid,
17574 } = backend_with_a_linked_stream(
17575 H2State::ClientPreface,
17576 BackendStatus::Connecting(Instant::now()),
17577 Ready::WRITABLE | Ready::HUP | Ready::ERROR,
17578 );
17579
17580 connection.end_stream(gid, &mut context);
17581 let received =
17582 drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
17583
17584 let preface = serializer::H2_PRI.as_bytes();
17585 assert!(
17586 received.starts_with(preface),
17587 "the connection preface must come first, got {received:?}"
17588 );
17589 let frames =
17590 peer_frames(&received[preface.len()..]).expect("whole frames after the preface");
17591 assert!(
17592 frames.iter().all(|(kind, _, id, _)| *id == 0 && *kind != 3),
17593 "only connection-level frames may follow, got {frames:?}"
17594 );
17595 }
17596
17597 /// sozu-proxy/sozu#1631, `ServerSettings` row: the preface is out and the
17598 /// server's SETTINGS are not read yet. Neither ending a stream nor
17599 /// linking one may kill the connection; `start_stream` arms a write, as
17600 /// it must for `Router::plan_connect`, whose pool scan links onto a
17601 /// `Connected` connection whatever its handshake state.
17602 ///
17603 /// TO SEE THIS RED: in [`ConnectionH2::dispatch_writable_state`], move
17604 /// `(H2State::ServerSettings, Position::Client(..))` back into the
17605 /// `Unexpected combination` arm. The connection then force-disconnects:
17606 /// `the shared connection must survive the handshake, got Error`. Red on
17607 /// `d5161919`, verified 2026-09-28.
17608 #[test]
17609 fn a_backend_stream_ended_during_the_handshake_keeps_the_connection() {
17610 let LinkedBackend {
17611 _pool,
17612 mut connection,
17613 mut peer,
17614 mut context,
17615 mut router,
17616 gid,
17617 } = backend_with_a_linked_stream(
17618 H2State::ServerSettings,
17619 BackendStatus::Connected,
17620 Ready::READABLE | Ready::HUP | Ready::ERROR,
17621 );
17622
17623 connection.end_stream(gid, &mut context);
17624 let received =
17625 drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
17626
17627 assert!(
17628 matches!(connection.core.state, H2State::ServerSettings),
17629 "the shared connection must survive the handshake, got {:?}",
17630 connection.core.state
17631 );
17632 assert_eq!(
17633 received,
17634 Vec::<u8>::new(),
17635 "the backend must receive nothing for a stream it never saw"
17636 );
17637 }
17638
17639 /// A backend may answer early, before the whole request reached it
17640 /// (RFC 9113 §8.1). When the stream then ends with the request received
17641 /// whole from the client but its tail still unsent toward the backend,
17642 /// the backend never sees END_STREAM: forgetting the stream without a
17643 /// reset leaves it open (half-closed) on the backend, holding one of the
17644 /// backend's MAX_CONCURRENT_STREAMS slots for this connection's lifetime
17645 /// (RFC 9113 §5.1.2). The stream must be cancelled, exactly once.
17646 ///
17647 /// TO SEE THIS RED: in the `Position::Client` arm of
17648 /// [`ConnectionH2::end_stream`], drop `&& stream.front.is_completed()`
17649 /// from `fully_completed`. The backend then reads no RST_STREAM: `the
17650 /// backend must read RST_STREAM(1, CANCEL) once`, `right` holding that
17651 /// frame and `left` nothing.
17652 #[test]
17653 fn a_backend_stream_answered_before_its_request_was_written_is_cancelled() {
17654 let LinkedBackend {
17655 _pool,
17656 mut connection,
17657 mut peer,
17658 mut context,
17659 mut router,
17660 gid,
17661 } = backend_with_a_linked_stream(
17662 H2State::Header,
17663 BackendStatus::Connected,
17664 Ready::READABLE | Ready::HUP | Ready::ERROR,
17665 );
17666 // A request received whole from the client: its HEADERS, then a
17667 // 4-byte body closed with END_STREAM.
17668 let kawa = &mut context.streams[gid].front;
17669 kawa.detached.status_line = kawa::StatusLine::Request {
17670 version: kawa::Version::V20,
17671 method: kawa::Store::Static(b"POST"),
17672 uri: kawa::Store::Static(b"/"),
17673 authority: kawa::Store::Static(b"example.com"),
17674 path: kawa::Store::Static(b"/"),
17675 };
17676 kawa.push_block(kawa::Block::StatusLine);
17677 kawa.push_block(kawa::Block::Header(kawa::Pair {
17678 key: kawa::Store::Static(b"content-length"),
17679 val: kawa::Store::Static(b"4"),
17680 }));
17681 kawa.push_block(kawa::Block::Flags(kawa::Flags {
17682 end_body: false,
17683 end_chunk: false,
17684 end_header: true,
17685 end_stream: false,
17686 }));
17687 kawa.push_block(kawa::Block::Chunk(kawa::Chunk {
17688 data: kawa::Store::Static(b"body"),
17689 }));
17690 kawa.push_block(kawa::Block::Flags(kawa::Flags {
17691 end_body: true,
17692 end_chunk: false,
17693 end_header: false,
17694 end_stream: true,
17695 }));
17696 kawa.body_size = kawa::BodySize::Length(4);
17697 kawa.parsing_phase = kawa::ParsingPhase::Terminated;
17698 // The backend grants the stream no send window: its HEADERS leave,
17699 // its body waits.
17700 context.streams[gid].back_window = 0;
17701 let sent = drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
17702 let frames = peer_frames(&sent).expect("whole frames");
17703 assert!(
17704 frames
17705 .iter()
17706 .any(|(kind, flags, id, _)| *kind == 1 && *id == 1 && flags & 0x1 == 0)
17707 && !frames.iter().any(|(kind, _, _, _)| *kind == 0),
17708 "premise: the HEADERS left without END_STREAM and no DATA, got {frames:?}"
17709 );
17710 let stream = &context.streams[gid];
17711 assert!(
17712 stream.front.consumed && stream.front.is_terminated() && !stream.front.is_completed(),
17713 "premise: the request was received whole but not written whole"
17714 );
17715 // The backend's complete response arrived: its END_STREAM was read.
17716 context.streams[gid].back_received_end_of_stream = true;
17717
17718 connection.end_stream(gid, &mut context);
17719 // Ending it again must not reset it a second time.
17720 connection.end_stream(gid, &mut context);
17721 let received =
17722 drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
17723
17724 assert_eq!(
17725 received,
17726 orphan_frame(3, 0, 1, 4, &(H2Error::Cancel as u32).to_be_bytes()),
17727 "the backend must read RST_STREAM(1, CANCEL) once"
17728 );
17729 assert!(
17730 connection.core.stream_table.is_empty(),
17731 "the stream is retired from the backend connection"
17732 );
17733 assert!(
17734 matches!(connection.core.state, H2State::Header),
17735 "the shared connection stays up, got {:?}",
17736 connection.core.state
17737 );
17738 }
17739
17740 /// The liveness half of the `ServerSettings` fix: the write readiness
17741 /// withdrawn while the server's SETTINGS are awaited comes back with
17742 /// them, and a request linked during the handshake goes out after the
17743 /// SETTINGS ACK.
17744 ///
17745 /// TO SEE THIS RED: in [`ConnectionH2::dispatch_writable_state`], move
17746 /// `(H2State::ServerSettings, Position::Client(..))` back into the
17747 /// `Unexpected combination` arm: the connection force-disconnects before
17748 /// the SETTINGS arrive, `premise: nothing is written before the server
17749 /// SETTINGS, got [] in Error`. Red on `d5161919`, verified 2026-09-28.
17750 #[test]
17751 fn a_request_linked_during_the_handshake_goes_out_after_the_settings() {
17752 use std::io::Write;
17753
17754 let LinkedBackend {
17755 _pool,
17756 mut connection,
17757 mut peer,
17758 mut context,
17759 mut router,
17760 gid,
17761 } = backend_with_a_linked_stream(
17762 H2State::ServerSettings,
17763 BackendStatus::Connected,
17764 Ready::READABLE | Ready::HUP | Ready::ERROR,
17765 );
17766 queue_request(&mut context, gid);
17767 let before = drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
17768 assert!(
17769 before.is_empty() && matches!(connection.core.state, H2State::ServerSettings),
17770 "premise: nothing is written before the server SETTINGS, got {before:?} in {:?}",
17771 connection.core.state
17772 );
17773
17774 peer.write_all(&orphan_frame(4, 0, 0, 0, b""))
17775 .expect("loopback write must complete");
17776 let after = drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
17777
17778 let frames = peer_frames(&after).expect("whole frames");
17779 assert!(
17780 frames
17781 .iter()
17782 .any(|(kind, flags, id, _)| *kind == 4 && *flags == parser::FLAG_ACK && *id == 0),
17783 "the server SETTINGS are acknowledged, got {frames:?}"
17784 );
17785 assert!(
17786 frames.iter().any(|(kind, _, id, _)| *kind == 1 && *id == 1),
17787 "the request linked during the handshake must go out, got {frames:?}"
17788 );
17789 }
17790
17791 /// A backend stream 1 whose response is framed by `content-length: 3`,
17792 /// its request sent and its response carrying 5 bytes of DATA, which
17793 /// makes Sōzu reset it with PROTOCOL_ERROR (RFC 9113 §8.1.1). Returns the
17794 /// fixture, ready for the frames the backend sends after the reset.
17795 fn backend_stream_reset_for_its_content_length() -> LinkedBackend {
17796 use std::io::Write;
17797
17798 let mut linked = backend_with_a_linked_stream(
17799 H2State::Header,
17800 BackendStatus::Connected,
17801 Ready::READABLE | Ready::HUP | Ready::ERROR,
17802 );
17803 let LinkedBackend {
17804 connection,
17805 peer,
17806 context,
17807 router,
17808 gid,
17809 ..
17810 } = &mut linked;
17811 queue_request(context, *gid);
17812 // Open on the client side, as a stream the router linked; `Link`
17813 // carries no frontend token for `reset_stream` to end.
17814 context.streams[*gid].state = StreamState::Link;
17815 let request = drive_and_read_backend(connection, peer, context, router);
17816 assert!(
17817 peer_frames(&request)
17818 .expect("whole frames")
17819 .iter()
17820 .any(|(kind, _, id, _)| *kind == 1 && *id == 1),
17821 "premise: the request went out on stream 1, got {request:?}"
17822 );
17823 // :status 200 (static index 8), then `content-length: 3` as a
17824 // literal without indexing.
17825 let mut head = vec![0x88, 0x00, 14];
17826 head.extend_from_slice(b"content-length");
17827 head.extend_from_slice(&[1, b'3']);
17828 let mut wire = orphan_frame(1, 0x4, 1, head.len() as u32, &head);
17829 wire.extend(orphan_frame(0, 0, 1, 5, b"hello"));
17830 peer.write_all(&wire).expect("loopback write must complete");
17831 let reset = drive_and_read_backend(connection, peer, context, router);
17832 let reset = peer_frames(&reset).expect("whole frames");
17833 assert!(
17834 reset
17835 .iter()
17836 .any(|(kind, _, id, payload)| *kind == 3 && *id == 1 && payload == &[0, 0, 0, 1]),
17837 "premise: stream 1 is reset with PROTOCOL_ERROR, got {reset:?}"
17838 );
17839 linked
17840 }
17841
17842 /// The trailer block adding `x-dyn: one` to the HPACK dynamic table
17843 /// (literal with incremental indexing) on stream 1, in one HEADERS frame
17844 /// or, with `split`, a HEADERS frame without END_HEADERS and a
17845 /// CONTINUATION frame (RFC 9113 §6.10).
17846 fn dynamic_trailer_block(split: bool) -> Vec<u8> {
17847 let mut trailer = vec![0x40, 5];
17848 trailer.extend_from_slice(b"x-dyn");
17849 trailer.extend_from_slice(&[3]);
17850 trailer.extend_from_slice(b"one");
17851 if split {
17852 let (first, rest) = trailer.split_at(4);
17853 let mut wire = orphan_frame(1, 0x1, 1, first.len() as u32, first);
17854 wire.extend(orphan_frame(9, 0x4, 1, rest.len() as u32, rest));
17855 wire
17856 } else {
17857 orphan_frame(1, 0x4 | 0x1, 1, trailer.len() as u32, &trailer)
17858 }
17859 }
17860
17861 /// RFC 9113 §5.1: after sozu resets a backend stream, the backend may
17862 /// still send frames it queued before reading the RST_STREAM, and sozu
17863 /// MUST ignore them. Here the backend sends a trailer block, in one
17864 /// HEADERS frame or split over a CONTINUATION frame, and another DATA
17865 /// frame on the reset stream. The trailer block adds `x-dyn: one` to the
17866 /// HPACK dynamic table, and the response on stream 3 refers to that
17867 /// entry, so it parses only if the ignored block was decoded whole
17868 /// (§4.3). What the backend had in flight counts no glitch.
17869 ///
17870 /// TO SEE THIS RED: in `ConnectionH2::handle_header_state`, drop the
17871 /// `was_reset_locally` branch: the trailer HEADERS is answered
17872 /// GOAWAY(STREAM_CLOSED); or, in the `H2State::Discard` arm,
17873 /// drop the `pending_discarded_block` store: the CONTINUATION is
17874 /// answered GOAWAY(PROTOCOL_ERROR) as standalone.
17875 #[test]
17876 fn frames_on_a_backend_stream_sozu_reset_are_ignored() {
17877 use std::io::Write;
17878
17879 for split in [true, false] {
17880 let LinkedBackend {
17881 _pool,
17882 mut connection,
17883 mut peer,
17884 mut context,
17885 mut router,
17886 ..
17887 } = backend_stream_reset_for_its_content_length();
17888 let glitches = connection.core.flood_detector.glitch_count();
17889
17890 let mut wire = dynamic_trailer_block(split);
17891 wire.extend(orphan_frame(0, 0x1, 1, 2, b"zz"));
17892 peer.write_all(&wire).expect("loopback write must complete");
17893 let after =
17894 drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
17895 let after = peer_frames(&after).expect("whole frames");
17896 assert!(
17897 !after.iter().any(|(kind, _, _, _)| *kind == 7 || *kind == 3),
17898 "split={split}: frames on the reset stream are ignored: no GOAWAY, no \
17899 RST_STREAM, got {after:?}"
17900 );
17901 assert!(
17902 matches!(connection.core.state, H2State::Header),
17903 "split={split}: the shared connection stays up, got {:?}",
17904 connection.core.state
17905 );
17906 assert_eq!(
17907 connection.core.flood_detector.glitch_count(),
17908 glitches,
17909 "split={split}: what the backend had in flight counts no glitch"
17910 );
17911
17912 // A second stream, answered with `:status 200` and the dynamic
17913 // entry the ignored trailer block added (index 62).
17914 let second = context
17915 .create_stream(Ulid::generate(), 1 << 16)
17916 .expect("test context must create a stream");
17917 assert!(
17918 connection.start_stream(second, &mut context),
17919 "the backend connection accepts a second stream"
17920 );
17921 queue_request(&mut context, second);
17922 context.streams[second].state = StreamState::Link;
17923 let request =
17924 drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
17925 assert!(
17926 peer_frames(&request)
17927 .expect("whole frames")
17928 .iter()
17929 .any(|(kind, _, id, _)| *kind == 1 && *id == 3),
17930 "premise: the second request went out on stream 3, got {request:?}"
17931 );
17932 peer.write_all(&orphan_frame(1, 0x4 | 0x1, 3, 2, &[0x88, 0xbe]))
17933 .expect("loopback write must complete");
17934 let last =
17935 drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
17936 let last = peer_frames(&last).expect("whole frames");
17937 assert!(
17938 !last.iter().any(|(kind, _, _, _)| *kind == 7),
17939 "split={split}: the second response decodes against the same dynamic table, \
17940 got {last:?}"
17941 );
17942 let buffer = context.streams[second].back.storage.buffer();
17943 let has_dynamic_field = context.streams[second].back.blocks.iter().any(|block| {
17944 matches!(block, kawa::Block::Header(kawa::Pair { key, val })
17945 if key.data(buffer) == b"x-dyn" && val.data(buffer) == b"one")
17946 });
17947 assert!(
17948 has_dynamic_field,
17949 "split={split}: the second response carries the field of the dynamic entry"
17950 );
17951 }
17952 }
17953
17954 /// DATA a backend sends on a stream Sōzu reset counts no glitch while it
17955 /// fits the stream's receive window, the 65 535 bytes Sōzu advertised
17956 /// and keeps crediting back: the backend may have had that much in
17957 /// flight when it read the reset (RFC 9113 §6.9). DATA beyond it counts
17958 /// one glitch per frame.
17959 ///
17960 /// TO SEE THIS RED: in `ConnectionH2::handle_header_state`, count a glitch for
17961 /// every frame ignored on a reset stream: `in-flight DATA counts no
17962 /// glitch`.
17963 #[test]
17964 fn data_on_a_reset_backend_stream_counts_a_glitch_beyond_its_window_only() {
17965 use std::io::Write;
17966
17967 let LinkedBackend {
17968 _pool,
17969 mut connection,
17970 mut peer,
17971 mut context,
17972 mut router,
17973 ..
17974 } = backend_stream_reset_for_its_content_length();
17975 let glitches = connection.core.flood_detector.glitch_count();
17976 // 5 bytes already arrived before the reset; four 16 000-byte frames
17977 // still fit in the 65 535-byte window.
17978 let chunk = vec![b'd'; 16_000];
17979 let mut wire = Vec::new();
17980 for _ in 0..4 {
17981 wire.extend(orphan_frame(0, 0, 1, chunk.len() as u32, &chunk));
17982 }
17983 peer.write_all(&wire).expect("loopback write must complete");
17984 drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
17985 assert_eq!(
17986 connection.core.flood_detector.glitch_count(),
17987 glitches,
17988 "in-flight DATA counts no glitch"
17989 );
17990
17991 peer.write_all(&orphan_frame(0, 0, 1, chunk.len() as u32, &chunk))
17992 .expect("loopback write must complete");
17993 let after = drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
17994 assert_eq!(
17995 connection.core.flood_detector.glitch_count(),
17996 glitches + 1,
17997 "DATA beyond the stream's window counts a glitch"
17998 );
17999 assert!(
18000 !peer_frames(&after)
18001 .expect("whole frames")
18002 .iter()
18003 .any(|(kind, _, _, _)| *kind == 7 || *kind == 3),
18004 "it is still ignored, not answered"
18005 );
18006 }
18007
18008 /// Header blocks a backend sends on a stream Sōzu reset count no glitch
18009 /// for the first two, a trailer section or an interim and a final
18010 /// response it may have had in flight (RFC 9113 §8.1), and one glitch
18011 /// each beyond: a peer repeating blocks on a reset stream trips the
18012 /// glitch limit and the connection ends with GOAWAY(ENHANCE_YOUR_CALM),
18013 /// instead of having every block HPACK-decoded for free.
18014 ///
18015 /// TO SEE THIS RED: in `ConnectionH2::handle_header_state`, count every header
18016 /// block on a reset stream as in flight: no GOAWAY ever comes.
18017 #[test]
18018 fn header_blocks_on_a_reset_backend_stream_beyond_two_count_glitches() {
18019 use std::io::Write;
18020
18021 let LinkedBackend {
18022 _pool,
18023 mut connection,
18024 mut peer,
18025 mut context,
18026 mut router,
18027 ..
18028 } = backend_stream_reset_for_its_content_length();
18029 // Each small write must reach the connection before the next drive,
18030 // not wait behind Nagle's algorithm for the previous one's ACK.
18031 peer.set_nodelay(true).expect("TCP_NODELAY must apply");
18032 // An explicit glitch limit, so the flood below trips it whatever the
18033 // default (`DEFAULT_MAX_GLITCH_COUNT`) is.
18034 connection.core.flood_detector = h2_flood_detector::H2FloodDetector::new(
18035 H2FloodConfig::from_optional(
18036 None,
18037 None,
18038 None,
18039 None,
18040 None,
18041 None,
18042 Some(100),
18043 None,
18044 None,
18045 None,
18046 None,
18047 None,
18048 None,
18049 None,
18050 ),
18051 connection.core.now,
18052 );
18053 let glitches = connection.core.flood_detector.glitch_count();
18054 // `:status 200`, alone, in one HEADERS frame ending the block.
18055 let block = || orphan_frame(1, 0x4, 1, 1, &[0x88]);
18056
18057 let mut wire = block();
18058 wire.extend(block());
18059 peer.write_all(&wire).expect("loopback write must complete");
18060 drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
18061 assert_eq!(
18062 connection.core.flood_detector.glitch_count(),
18063 glitches,
18064 "two header blocks may be in flight"
18065 );
18066
18067 peer.write_all(&block())
18068 .expect("loopback write must complete");
18069 drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
18070 assert_eq!(
18071 connection.core.flood_detector.glitch_count(),
18072 glitches + 1,
18073 "a third header block counts a glitch"
18074 );
18075
18076 let mut received = Vec::new();
18077 for _ in 0..20 {
18078 let mut wire = Vec::new();
18079 for _ in 0..100 {
18080 wire.extend(block());
18081 }
18082 if peer.write_all(&wire).is_err() {
18083 break;
18084 }
18085 received.extend(drive_and_read_backend(
18086 &mut connection,
18087 &mut peer,
18088 &mut context,
18089 &mut router,
18090 ));
18091 if peer_frames(&received)
18092 .map(|frames| frames.iter().any(|(kind, _, _, _)| *kind == 7))
18093 .unwrap_or(false)
18094 {
18095 break;
18096 }
18097 }
18098 let frames = peer_frames(&received).expect("whole frames");
18099 assert!(
18100 frames.iter().any(|(kind, _, _, payload)| *kind == 7
18101 && payload.get(4..8) == Some(&[0, 0, 0, 0xb][..])),
18102 "repeated header blocks end the connection with GOAWAY(ENHANCE_YOUR_CALM), got {frames:?}"
18103 );
18104 }
18105
18106 /// Empty DATA frames without END_STREAM on a stream Sōzu reset count
18107 /// toward the empty-DATA flood limit as on a live stream
18108 /// (CVE-2019-9518): they carry no payload to charge against the stream's
18109 /// window, so without it a peer could send them for free. A flood of
18110 /// them ends the connection with GOAWAY(ENHANCE_YOUR_CALM).
18111 ///
18112 /// TO SEE THIS RED: in `ConnectionH2::handle_header_state`, drop the
18113 /// `record_empty_data_frame` call of the `was_reset_locally` branch: no
18114 /// GOAWAY ever comes.
18115 #[test]
18116 fn empty_data_frames_on_a_reset_backend_stream_count_toward_the_flood_limit() {
18117 use std::io::Write;
18118
18119 let LinkedBackend {
18120 _pool,
18121 mut connection,
18122 mut peer,
18123 mut context,
18124 mut router,
18125 ..
18126 } = backend_stream_reset_for_its_content_length();
18127 peer.set_nodelay(true).expect("TCP_NODELAY must apply");
18128 // An explicit empty-DATA limit, so the flood below trips it whatever the
18129 // default (`DEFAULT_MAX_EMPTY_DATA_PER_WINDOW`) is. The glitch limit
18130 // stays the default: the GOAWAY must come from the empty-DATA count.
18131 connection.core.flood_detector = h2_flood_detector::H2FloodDetector::new(
18132 H2FloodConfig::from_optional(
18133 None,
18134 None,
18135 None,
18136 Some(100),
18137 None,
18138 None,
18139 None,
18140 None,
18141 None,
18142 None,
18143 None,
18144 None,
18145 None,
18146 None,
18147 ),
18148 connection.core.now,
18149 );
18150 let mut received = Vec::new();
18151 for _ in 0..50 {
18152 let mut wire = Vec::new();
18153 for _ in 0..100 {
18154 wire.extend(orphan_frame(0, 0, 1, 0, b""));
18155 }
18156 if peer.write_all(&wire).is_err() {
18157 break;
18158 }
18159 received.extend(drive_and_read_backend(
18160 &mut connection,
18161 &mut peer,
18162 &mut context,
18163 &mut router,
18164 ));
18165 if peer_frames(&received)
18166 .map(|frames| frames.iter().any(|(kind, _, _, _)| *kind == 7))
18167 .unwrap_or(false)
18168 {
18169 break;
18170 }
18171 }
18172 let frames = peer_frames(&received).expect("whole frames");
18173 assert!(
18174 frames.iter().any(|(kind, _, _, payload)| *kind == 7
18175 && payload.get(4..8) == Some(&[0, 0, 0, 0xb][..])),
18176 "a flood of empty DATA frames ends the connection with \
18177 GOAWAY(ENHANCE_YOUR_CALM), got {frames:?}"
18178 );
18179 }
18180
18181 /// A backend stream Sōzu reset from its write pass, because its request
18182 /// failed after its head left, stays tracked until its end: DATA the
18183 /// backend then sends on it is ignored (RFC 9113 §5.1), never queued for
18184 /// the client nor counted against the response.
18185 ///
18186 /// TO SEE THIS RED: in `ConnectionH2::handle_header_state`, drop the
18187 /// `was_reset_locally` branch: the DATA reaches the stream and is counted
18188 /// in `back_data_received`.
18189 #[test]
18190 fn data_on_a_tracked_backend_stream_sozu_reset_is_ignored() {
18191 use std::io::Write;
18192
18193 let LinkedBackend {
18194 _pool,
18195 mut connection,
18196 mut peer,
18197 mut context,
18198 mut router,
18199 gid,
18200 } = backend_with_a_linked_stream(
18201 H2State::Header,
18202 BackendStatus::Connected,
18203 Ready::READABLE | Ready::HUP | Ready::ERROR,
18204 );
18205 // A request whose body is still to come: its head leaves, the
18206 // stream stays open on the request side.
18207 queue_request(&mut context, gid);
18208 let front = &mut context.streams[gid].front;
18209 if let Some(kawa::Block::Flags(flags)) = front.blocks.back_mut() {
18210 flags.end_stream = false;
18211 }
18212 front.body_size = kawa::BodySize::Chunked;
18213 front.parsing_phase = kawa::ParsingPhase::Chunks { first: true };
18214 context.streams[gid].state = StreamState::Link;
18215 drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
18216 // The request side then fails: the write pass resets the stream and
18217 // keeps it tracked.
18218 context.streams[gid]
18219 .front
18220 .parsing_phase
18221 .error("request failed".into());
18222 connection.core.readiness.arm_writable();
18223 let reset = drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
18224 assert!(
18225 peer_frames(&reset)
18226 .expect("whole frames")
18227 .iter()
18228 .any(|(kind, _, id, _)| *kind == 3 && *id == 1),
18229 "premise: the write pass resets stream 1, got {reset:?}"
18230 );
18231 assert!(
18232 connection.core.stream_table.get(1).is_some()
18233 && connection.core.stream_table.rst_sent_contains(1),
18234 "premise: the reset stream is still tracked"
18235 );
18236
18237 peer.write_all(&orphan_frame(0, 0, 1, 5, b"hello"))
18238 .expect("loopback write must complete");
18239 let after = drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
18240 assert_eq!(
18241 context.streams[gid].back_data_received, 0,
18242 "the DATA is not counted against the response"
18243 );
18244 assert!(
18245 !context.streams[gid]
18246 .back
18247 .blocks
18248 .iter()
18249 .any(|block| matches!(block, kawa::Block::Chunk(_))),
18250 "the DATA is not queued for the client"
18251 );
18252 assert!(
18253 !peer_frames(&after)
18254 .expect("whole frames")
18255 .iter()
18256 .any(|(kind, _, _, _)| *kind == 7),
18257 "the connection stays up"
18258 );
18259 }
18260
18261 /// The client-facing side of RFC 9113 §5.1: a request framed by
18262 /// `content-length: 3` whose DATA carries 5 bytes is reset by Sōzu
18263 /// (§8.1.1); the DATA and the trailer block the client had already sent
18264 /// on that stream are ignored, and the connection stays up.
18265 ///
18266 /// TO SEE THIS RED: in `ConnectionH2::handle_header_state`, drop the
18267 /// `was_reset_locally` branch: the trailer HEADERS is answered
18268 /// GOAWAY(STREAM_CLOSED).
18269 #[test]
18270 fn frames_on_a_client_stream_sozu_reset_are_ignored() {
18271 use std::io::Write;
18272
18273 let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 20, 16_384)));
18274 let (mut connection, mut peer) = test_h2_connection(&pool, None);
18275 let mut context = test_context(&pool);
18276 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
18277 connection.core.state = H2State::Header;
18278 connection
18279 .core
18280 .stream_table
18281 .set_expect_read(Some((H2StreamId::Zero, 9)));
18282 let block = crate::protocol::mux::hpack::Encoder::new().encode([
18283 (&b":method"[..], &b"POST"[..]),
18284 (&b":scheme"[..], &b"https"[..]),
18285 (&b":authority"[..], &b"example.com"[..]),
18286 (&b":path"[..], &b"/upload"[..]),
18287 (&b"content-length"[..], &b"3"[..]),
18288 ]);
18289 let mut wire = orphan_frame(1, parser::FLAG_END_HEADERS, 1, block.len() as u32, &block);
18290 wire.extend(orphan_frame(0, 0, 1, 5, b"hello"));
18291 peer.write_all(&wire).expect("loopback write must complete");
18292 let reset = drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
18293 let reset = peer_frames(&reset).expect("whole frames");
18294 assert!(
18295 reset
18296 .iter()
18297 .any(|(kind, _, id, payload)| *kind == 3 && *id == 1 && payload == &[0, 0, 0, 1]),
18298 "premise: stream 1 is reset with PROTOCOL_ERROR, got {reset:?}"
18299 );
18300
18301 let mut wire = orphan_frame(0, 0, 1, 2, b"zz");
18302 wire.extend(dynamic_trailer_block(false));
18303 peer.write_all(&wire).expect("loopback write must complete");
18304 let after = drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
18305 let after = peer_frames(&after).expect("whole frames");
18306 assert!(
18307 !after.iter().any(|(kind, _, _, _)| *kind == 7 || *kind == 3),
18308 "frames on the reset stream are ignored: no GOAWAY, no RST_STREAM, got {after:?}"
18309 );
18310 assert!(
18311 matches!(connection.core.state, H2State::Header),
18312 "the client connection stays up, got {:?}",
18313 connection.core.state
18314 );
18315 }
18316
18317 /// A stream Sōzu refuses at its HEADERS frame (here because
18318 /// SETTINGS_MAX_CONCURRENT_STREAMS is reached) is answered
18319 /// RST_STREAM(REFUSED_STREAM) and its field block discarded, and the
18320 /// CONTINUATION frame completing that block (RFC 9113 §6.10) is
18321 /// discarded with it, not taken for a standalone CONTINUATION.
18322 ///
18323 /// TO SEE THIS RED: in the `H2State::Discard` arm of
18324 /// [`ConnectionH2::handle_read`], drop the `pending_discarded_block`
18325 /// store: the CONTINUATION is answered GOAWAY(PROTOCOL_ERROR).
18326 #[test]
18327 fn a_continuation_of_a_refused_header_block_is_discarded_with_it() {
18328 use std::io::Write;
18329
18330 let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 20, 16_384)));
18331 let (mut connection, mut peer) = test_h2_connection(&pool, None);
18332 let mut context = test_context(&pool);
18333 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
18334 connection.core.state = H2State::Header;
18335 connection
18336 .core
18337 .stream_table
18338 .set_expect_read(Some((H2StreamId::Zero, 9)));
18339 connection
18340 .core
18341 .local_settings
18342 .settings_max_concurrent_streams = 0;
18343 let block = crate::protocol::mux::hpack::Encoder::new().encode([
18344 (&b":method"[..], &b"GET"[..]),
18345 (&b":scheme"[..], &b"https"[..]),
18346 (&b":authority"[..], &b"example.com"[..]),
18347 (&b":path"[..], &b"/"[..]),
18348 ]);
18349 let (first, rest) = block.split_at(block.len() / 2);
18350 let mut wire = orphan_frame(1, parser::FLAG_END_STREAM, 1, first.len() as u32, first);
18351 wire.extend(orphan_frame(
18352 9,
18353 parser::FLAG_END_HEADERS,
18354 1,
18355 rest.len() as u32,
18356 rest,
18357 ));
18358 peer.write_all(&wire).expect("loopback write must complete");
18359 let answer = drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
18360 let answer = peer_frames(&answer).expect("whole frames");
18361 assert!(
18362 answer
18363 .iter()
18364 .any(|(kind, _, id, payload)| *kind == 3 && *id == 1 && payload == &[0, 0, 0, 7]),
18365 "the stream is refused, got {answer:?}"
18366 );
18367 assert!(
18368 !answer.iter().any(|(kind, _, _, _)| *kind == 7),
18369 "the CONTINUATION is discarded with its block, got {answer:?}"
18370 );
18371 assert!(
18372 matches!(connection.core.state, H2State::Header),
18373 "the client connection stays up, got {:?}",
18374 connection.core.state
18375 );
18376 }
18377
18378 /// A refused header block is held to `max_header_list_size` like an
18379 /// accepted one, its first fragment included: a 16 000-byte HEADERS frame
18380 /// and four 12 500-byte CONTINUATION frames exceed the 65 536-byte limit
18381 /// only with that first fragment, and end the connection with
18382 /// GOAWAY(ENHANCE_YOUR_CALM) (CVE-2024-27316).
18383 ///
18384 /// TO SEE THIS RED: in [`ConnectionH2::discard_field_block`], drop the
18385 /// `begin_header_block_if_new` call: the block is read to its end, and
18386 /// the test fails once its 30-second deadline passes without a GOAWAY.
18387 #[test]
18388 fn a_refused_header_block_counts_its_first_fragment_toward_its_size() {
18389 use std::io::Write;
18390
18391 let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 20, 16_384)));
18392 let (mut connection, mut peer) = test_h2_connection(&pool, None);
18393 let mut context = test_context(&pool);
18394 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
18395 connection.core.state = H2State::Header;
18396 connection
18397 .core
18398 .stream_table
18399 .set_expect_read(Some((H2StreamId::Zero, 9)));
18400 connection
18401 .core
18402 .local_settings
18403 .settings_max_concurrent_streams = 0;
18404 let first = vec![0x88; 16_000];
18405 let mut wire = orphan_frame(1, 0, 1, first.len() as u32, &first);
18406 let rest = vec![0x88; 12_500];
18407 for _ in 0..4 {
18408 wire.extend(orphan_frame(9, 0, 1, rest.len() as u32, &rest));
18409 }
18410 // The peer cannot write all of it before the connection reads, so
18411 // write it from a thread while the connection is driven.
18412 let mut writer = peer.try_clone().expect("the peer socket must clone");
18413 let sender = std::thread::spawn(move || writer.write_all(&wire));
18414 let enhance_your_calm = |frames: &[PeerFrame]| {
18415 frames.iter().any(|(kind, _, _, payload)| {
18416 *kind == 7 && payload.get(4..8) == Some(&[0, 0, 0, 0xb][..])
18417 })
18418 };
18419 // Drive until the GOAWAY reaches the peer, not for a fixed number of
18420 // passes: on a loaded host the writer thread can be descheduled for
18421 // all of them, and the GOAWAY the read of the last fragment queues is
18422 // only flushed by the next pass's `writable()`.
18423 let deadline = Instant::now() + Duration::from_secs(30);
18424 let mut received = Vec::new();
18425 while Instant::now() < deadline {
18426 received.extend(drive_and_read_backend(
18427 &mut connection,
18428 &mut peer,
18429 &mut context,
18430 &mut router,
18431 ));
18432 if peer_frames(&received).is_ok_and(|frames| enhance_your_calm(&frames)) {
18433 break;
18434 }
18435 }
18436 sender
18437 .join()
18438 .expect("the writer thread must not panic")
18439 .expect("the peer must write the whole block");
18440 let frames = peer_frames(&received).expect("whole frames");
18441 assert!(
18442 enhance_your_calm(&frames),
18443 "the oversized refused block ends the connection with GOAWAY(ENHANCE_YOUR_CALM), \
18444 got {frames:?}"
18445 );
18446 }
18447
18448 /// sozu-proxy/sozu#1631, reaper form: `cancel_timed_out_streams` has the
18449 /// same shape as `end_stream` for a backend stream reaped before its
18450 /// HEADERS left, and must forget it the same way.
18451 ///
18452 /// TO SEE THIS RED: in [`ConnectionH2::cancel_timed_out_streams`], make
18453 /// `never_opened` `false`. The stream is then reset: `a stream reaped
18454 /// before its HEADERS left must not be reset`. Red on `d5161919`,
18455 /// verified 2026-09-28.
18456 #[test]
18457 fn a_backend_stream_reaped_before_its_headers_left_is_not_reset() {
18458 let LinkedBackend {
18459 _pool,
18460 mut connection,
18461 mut peer,
18462 mut context,
18463 mut router,
18464 gid: _,
18465 } = backend_with_a_linked_stream(
18466 H2State::Header,
18467 BackendStatus::Connected,
18468 Ready::READABLE | Ready::HUP | Ready::ERROR,
18469 );
18470 connection.core.readiness.interest.remove(Ready::WRITABLE);
18471
18472 context.now += Duration::from_secs(31);
18473 connection.cancel_timed_out_streams(&mut context, &mut EndpointClient(&mut router));
18474 assert!(
18475 connection.core.stream_table.is_empty(),
18476 "premise: the idle guard reaped stream 1"
18477 );
18478 assert!(
18479 connection.core.control_tx.pending().is_empty()
18480 && !connection.core.readiness.interest.is_writable()
18481 && connection.core.output.is_empty(),
18482 "a stream reaped before its HEADERS left must not be reset"
18483 );
18484 let received =
18485 drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
18486 assert_eq!(
18487 received,
18488 Vec::<u8>::new(),
18489 "and nothing reaches the backend"
18490 );
18491 }
18492
18493 /// sozu-proxy/sozu#1631, frontend-error form (found by the adversarial
18494 /// review of the first fix): the H1 frontend rejects a malformed request
18495 /// body and answers 400 itself, which leaves the request kawa in `Error`
18496 /// while its backend stream is still registered and its HEADERS never
18497 /// left. The next write pass used to hand it to the converter, whose
18498 /// `initialize` queues a RST_STREAM for that idle stream.
18499 ///
18500 /// TO SEE THIS RED: in [`ConnectionH2::poll_write_target`]'s
18501 /// `H2WritePhase::Prepare` arm, make `never_opened_error` `false`. The
18502 /// backend then reads `RST_STREAM(1)`: `no frame may name stream 1`.
18503 /// Red on `6eef0ba1`, verified 2026-09-28.
18504 #[test]
18505 fn a_backend_stream_whose_request_failed_before_its_headers_left_is_not_reset() {
18506 let LinkedBackend {
18507 _pool,
18508 mut connection,
18509 mut peer,
18510 mut context,
18511 mut router,
18512 gid,
18513 } = backend_with_a_linked_stream(
18514 H2State::Header,
18515 BackendStatus::Connected,
18516 Ready::READABLE | Ready::HUP | Ready::ERROR,
18517 );
18518 queue_request(&mut context, gid);
18519 // What `kawa::h1::parse` leaves on a malformed chunked body.
18520 context.streams[gid]
18521 .front
18522 .parsing_phase
18523 .error(kawa::ParsingErrorKind::Processing {
18524 message: "malformed chunk",
18525 });
18526 assert!(
18527 context.streams[gid].front.is_error() && !context.streams[gid].front.consumed,
18528 "premise: the request failed before any byte left"
18529 );
18530
18531 let received =
18532 drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
18533
18534 let frames = peer_frames(&received).expect("whole frames");
18535 assert!(
18536 frames.iter().all(|(_, _, id, _)| *id != 1),
18537 "no frame may name stream 1, which the backend never saw: {frames:?}"
18538 );
18539 assert!(
18540 matches!(connection.core.state, H2State::Header),
18541 "the shared connection stays up, got {:?}",
18542 connection.core.state
18543 );
18544 }
18545
18546 /// sozu-proxy/sozu#1631, parked form: the stream's HEADERS were encoded
18547 /// by a write pass, but the socket took none of it, so the block is
18548 /// parked in `front.out` (the #1627 situation on `Position::Client`).
18549 /// The backend has still not seen the stream: no RST for it, and since
18550 /// #1630 the dropped block resets the encoder's table, so the next
18551 /// stream's block still decodes on the backend.
18552 ///
18553 /// TO SEE THIS RED: same edit as
18554 /// `a_backend_stream_ended_before_its_headers_left_is_not_reset`. The
18555 /// backend then reads a RST_STREAM for stream 1, which it never opened:
18556 /// `no frame may name stream 1`. Red on `d5161919`, verified 2026-09-28.
18557 #[test]
18558 fn a_backend_stream_ended_with_its_headers_parked_is_not_reset() {
18559 let pool = make_pool_for_invariant_16();
18560 let (socket, _peer) = connected_socket();
18561 let backend = Rc::new(RefCell::new(crate::backends::Backend::new(
18562 "parked-backend",
18563 "127.0.0.1:2".parse().expect("backend address must parse"),
18564 None,
18565 None,
18566 None,
18567 )));
18568 let mut registry = crate::protocol::mux::BackendRegistry::default();
18569 let mut connection = H2Shell::new(
18570 Ulid::generate(),
18571 PacedSocket {
18572 stream: socket,
18573 budget: 0,
18574 wire: Vec::new(),
18575 inbound: std::collections::VecDeque::new(),
18576 },
18577 Position::Client(
18578 "parked-cluster".into(),
18579 registry.id_for(&backend),
18580 BackendStatus::Connected,
18581 ),
18582 &mut PoolBufferSource::new(Rc::downgrade(&pool)),
18583 H2FloodConfig::default(),
18584 H2ConnectionConfig::default(),
18585 Duration::from_secs(30),
18586 None,
18587 Duration::from_secs(30),
18588 Some((H2StreamId::Zero, 9)),
18589 Ready::READABLE | Ready::WRITABLE | Ready::HUP | Ready::ERROR,
18590 )
18591 .expect("a pool with free buffers must yield an H2 connection");
18592 connection.core.state = H2State::Header;
18593 let mut context = test_context(&pool);
18594 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
18595 let first = context
18596 .create_stream(Ulid::generate(), 1 << 16)
18597 .expect("test context must create a stream");
18598 queue_request(&mut context, first);
18599 assert!(
18600 connection.start_stream(first, &mut context),
18601 "premise: stream 1"
18602 );
18603 connection.core.readiness.event.insert(Ready::WRITABLE);
18604 connection.writable(&mut context, EndpointClient(&mut router));
18605 assert!(
18606 connection.socket.wire.is_empty()
18607 && !context.streams[first].front.out.is_empty()
18608 && !context.streams[first].front.consumed,
18609 "premise: stream 1's HEADERS were encoded and parked unsent"
18610 );
18611
18612 connection.end_stream(first, &mut context);
18613 let third = context
18614 .create_stream(Ulid::generate(), 1 << 16)
18615 .expect("test context must create a stream");
18616 queue_request(&mut context, third);
18617 assert!(
18618 connection.start_stream(third, &mut context),
18619 "premise: stream 3"
18620 );
18621 connection.socket.budget = usize::MAX;
18622 drive_both_ways(&mut connection, &mut context, &mut router);
18623
18624 let frames = peer_frames(&connection.socket.wire).expect("whole frames");
18625 assert!(
18626 frames.iter().all(|(_, _, id, _)| *id != 1),
18627 "no frame may name stream 1, which the backend never saw: {frames:?}"
18628 );
18629 let mut decoder = crate::protocol::mux::hpack::Decoder::new();
18630 let decoded: Vec<(u32, bool)> = frames
18631 .iter()
18632 .filter(|(kind, ..)| *kind == 1)
18633 .map(|(_, _, id, payload)| (*id, decoder.decode_with_cb(payload, |_, _| {}).is_ok()))
18634 .collect();
18635 assert_eq!(
18636 decoded,
18637 vec![(3, true)],
18638 "stream 3's block must decode with the backend's own table"
18639 );
18640 }
18641
18642 /// #1597, with #1604's split: skipping an orphaned remainder turns
18643 /// READABLE back on even while output is queued, and leaves that output
18644 /// alone. The skip reads into `zero`, which is input only, so there is
18645 /// nothing to wait for; the queued frame still leaves in order.
18646 ///
18647 /// Replaces the pre-#1604 test that pinned the opposite (READABLE kept
18648 /// off while zero held output), for the pre-image, where `zero` also held
18649 /// our output and a read into it would have parsed that output.
18650 ///
18651 /// TO SEE THIS RED: in [`ConnectionH2::skip_orphaned_data_payload`], only
18652 /// insert READABLE when `self.output.is_empty()`. The test fails with
18653 /// `the skip must read on while output is queued`.
18654 #[test]
18655 fn skipping_an_orphaned_payload_reads_on_while_output_is_queued() {
18656 let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 20, 16_384)));
18657 let (mut connection, _peer) = test_h2_connection(&pool, None);
18658 let window_update = orphan_frame(8, 0, 1, 4, &16_384u32.to_be_bytes());
18659 connection.core.output.push(&window_update);
18660 connection.core.readiness.interest.remove(Ready::READABLE);
18661
18662 connection.core.skip_orphaned_data_payload(6, 10);
18663
18664 assert!(
18665 connection.core.readiness.interest.is_readable(),
18666 "the skip must read on while output is queued"
18667 );
18668 assert_eq!(
18669 connection.core.stream_table.expect_read(),
18670 Some((H2StreamId::Zero, 6)),
18671 "the remainder must still be scheduled for skipping"
18672 );
18673 assert_eq!(
18674 connection.core.output_pending(),
18675 &window_update[..],
18676 "the skip must leave the queued output untouched"
18677 );
18678 }
18679
18680 /// A backend (client-side) H2 connection starts write-only, sends its
18681 /// preface and SETTINGS, and must then READ, for the server's SETTINGS.
18682 /// The pre-image turned READABLE on as a side effect of flushing `zero`;
18683 /// with the output queue separate from the input, the
18684 /// `(ClientSettings, Client)` arm turns it on itself.
18685 ///
18686 /// TO SEE THIS RED: in [`ConnectionH2::dispatch_writable_state`]'s
18687 /// `(H2State::ClientSettings, Position::Client(..))` arm, delete
18688 /// `self.readiness.interest.insert(Ready::READABLE);`. The test fails
18689 /// with `a backend connection must read once its preface is out`.
18690 #[test]
18691 fn a_backend_connection_reads_once_its_preface_is_out() {
18692 use std::io::Read;
18693
18694 let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 20, 16_384)));
18695 let (socket, mut peer) = connected_socket();
18696 let backend = Rc::new(RefCell::new(crate::backends::Backend::new(
18697 "preface-backend",
18698 "127.0.0.1:2".parse().expect("backend address must parse"),
18699 None,
18700 None,
18701 None,
18702 )));
18703 let mut registry = crate::protocol::mux::BackendRegistry::default();
18704 let mut connection = H2Shell::new(
18705 Ulid::generate(),
18706 socket,
18707 Position::Client(
18708 "preface-cluster".into(),
18709 registry.id_for(&backend),
18710 BackendStatus::Connected,
18711 ),
18712 &mut PoolBufferSource::new(Rc::downgrade(&pool)),
18713 H2FloodConfig::default(),
18714 H2ConnectionConfig::default(),
18715 Duration::from_secs(30),
18716 None,
18717 Duration::from_secs(30),
18718 None,
18719 Ready::WRITABLE | Ready::HUP | Ready::ERROR,
18720 )
18721 .expect("a pool with free buffers must yield an H2 connection");
18722 let mut context = test_context(&pool);
18723 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
18724
18725 for _ in 0..2 {
18726 connection.core.readiness.event.insert(Ready::WRITABLE);
18727 connection.writable(&mut context, EndpointClient(&mut router));
18728 }
18729
18730 let mut preface = [0u8; 24];
18731 peer.read_exact(&mut preface)
18732 .expect("the backend must receive the client preface");
18733 assert_eq!(&preface[..], serializer::H2_PRI.as_bytes());
18734 assert!(
18735 matches!(connection.core.state, H2State::ServerSettings),
18736 "premise: the preface went out, got {:?}",
18737 connection.core.state
18738 );
18739 assert!(
18740 connection.core.readiness.interest.is_readable(),
18741 "a backend connection must read once its preface is out"
18742 );
18743 }
18744
18745 /// A peer that sends while never reading cannot grow the output queue
18746 /// without bound: once one buffer's worth is queued, reading stops, as
18747 /// HAProxy's demux stops on a full `mbuf` (`H2_CF_DEM_MROOM`), and it
18748 /// resumes once the queue drains below that.
18749 ///
18750 /// TO SEE THIS RED: in [`ConnectionH2::poll_read_target`], delete the
18751 /// `self.output.len() >= self.output_read_cap()` early return. The test
18752 /// fails with `a full output queue must stop reading`.
18753 #[test]
18754 fn a_full_output_queue_stops_reading_until_it_drains() {
18755 let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 20, 16_384)));
18756 let (mut connection, _peer) = test_h2_connection(&pool, None);
18757 let mut context = test_context(&pool);
18758 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
18759 connection.core.state = H2State::Header;
18760 connection
18761 .core
18762 .stream_table
18763 .set_expect_read(Some((H2StreamId::Zero, 9)));
18764 let cap = connection.core.output_read_cap();
18765 connection.core.output.push(&vec![0; cap]);
18766 connection.core.readiness.interest.insert(Ready::READABLE);
18767 connection.core.readiness.event.insert(Ready::READABLE);
18768
18769 connection.readable(&mut context, EndpointClient(&mut router));
18770
18771 assert!(
18772 !connection.core.readiness.interest.is_readable(),
18773 "a full output queue must stop reading"
18774 );
18775 assert!(
18776 connection.core.readiness.event.is_readable(),
18777 "the READABLE event must be kept for the read that follows the drain"
18778 );
18779 connection.core.consume_output(1);
18780 assert!(
18781 connection.core.readiness.interest.is_readable(),
18782 "draining below the cap must restore reading"
18783 );
18784 }
18785
18786 /// Clearing the output queue, as a peer that hung up while the
18787 /// connection drains does, also ends the read cap's hold on READABLE:
18788 /// otherwise `reads_wait_for_output` would outlive the output it waited on
18789 /// and the shutdown drive's reads would stay withdrawn.
18790 ///
18791 /// TO SEE THIS RED: in `ConnectionH2::clear_output`, drop everything but
18792 /// `self.output.clear();`. The test then fails with `a cleared queue must
18793 /// release the read cap`. Verified 2026-09-27.
18794 #[test]
18795 fn clearing_the_output_releases_the_read_cap() {
18796 let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 20, 16_384)));
18797 let (mut connection, _peer) = test_h2_connection(&pool, None);
18798 let mut context = test_context(&pool);
18799 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
18800 connection.core.state = H2State::Header;
18801 connection
18802 .core
18803 .stream_table
18804 .set_expect_read(Some((H2StreamId::Zero, 9)));
18805 let cap = connection.core.output_read_cap();
18806 connection.core.output.push(&vec![0; cap]);
18807 connection.core.readiness.interest.insert(Ready::READABLE);
18808 connection.core.readiness.event.insert(Ready::READABLE);
18809 connection.readable(&mut context, EndpointClient(&mut router));
18810 assert!(
18811 connection.core.reads_wait_for_output
18812 && !connection.core.readiness.interest.is_readable(),
18813 "premise: the read cap withdrew READABLE"
18814 );
18815
18816 connection.core.clear_output();
18817
18818 assert!(
18819 !connection.core.reads_wait_for_output
18820 && connection.core.readiness.interest.is_readable(),
18821 "a cleared queue must release the read cap"
18822 );
18823 }
18824
18825 /// #1597, with #1604's split: a frame header read while output is still
18826 /// queued lands in `zero` and leaves that output intact, and the answer
18827 /// to the frame read queues BEHIND it. A stalled WINDOW_UPDATE flush
18828 /// leaves exactly that state; the CI e2e suite once caught the
18829 /// pre-image, where both shared `zero`, through `handle_header_state`'s
18830 /// `debug_assert!`, with `zero` holding a stream-1 WINDOW_UPDATE followed
18831 /// by the peer's next DATA header.
18832 ///
18833 /// Replaces the pre-#1604 test that pinned the pre-image's cure (no frame
18834 /// header read at all while zero held unflushed output until the flush):
18835 /// the hazard it guarded no longer exists, and what the peer must receive
18836 /// is unchanged and still asserted below.
18837 ///
18838 /// TO SEE THIS RED: in [`ConnectionH2::handle_ping_frame`], queue the ACK
18839 /// in front of the output instead of behind it (`self.output.clear()`
18840 /// before `push_frames`). The test fails with `the PING ACK must queue
18841 /// behind the WINDOW_UPDATE`.
18842 #[test]
18843 fn a_frame_header_read_leaves_queued_output_intact() {
18844 use std::io::{Read, Write};
18845
18846 let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 20, 16_384)));
18847 let (mut connection, mut peer) = test_h2_connection(&pool, None);
18848 peer.set_nonblocking(true)
18849 .expect("the loopback peer must switch to non-blocking");
18850 let mut context = test_context(&pool);
18851 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
18852 connection.core.state = H2State::Header;
18853 connection
18854 .core
18855 .stream_table
18856 .set_expect_read(Some((H2StreamId::Zero, 9)));
18857
18858 // What a stalled WINDOW_UPDATE flush leaves behind.
18859 let window_update = orphan_frame(8, 0, 1, 4, &16_384u32.to_be_bytes());
18860 connection.core.output.push(&window_update);
18861 connection.core.readiness.interest.insert(Ready::READABLE);
18862
18863 peer.write_all(&orphan_ping())
18864 .expect("loopback write must complete");
18865 let mut expected = window_update.clone();
18866 expected.extend(orphan_frame(6, parser::FLAG_ACK, 0, 8, b"pingpong"));
18867 for _ in 0..64 {
18868 connection.core.readiness.event.insert(Ready::READABLE);
18869 connection.readable(&mut context, EndpointClient(&mut router));
18870 if connection.core.output.len() >= expected.len() {
18871 break;
18872 }
18873 std::thread::yield_now();
18874 }
18875 assert_eq!(
18876 connection.core.output_pending(),
18877 &expected[..],
18878 "the PING ACK must queue behind the WINDOW_UPDATE"
18879 );
18880
18881 // Driven the way `Mux` does, the queue leaves in order.
18882 let mut received = Vec::new();
18883 for _ in 0..256 {
18884 connection
18885 .core
18886 .readiness
18887 .event
18888 .insert(Ready::READABLE | Ready::WRITABLE);
18889 connection.core.readiness.interest.insert(Ready::WRITABLE);
18890 if connection.core.readiness.filter_interest().is_writable() {
18891 connection.writable(&mut context, EndpointClient(&mut router));
18892 }
18893 if connection.core.readiness.filter_interest().is_readable() {
18894 connection.readable(&mut context, EndpointClient(&mut router));
18895 }
18896 let mut buf = [0u8; 256];
18897 if let Ok(n) = peer.read(&mut buf) {
18898 received.extend_from_slice(&buf[..n]);
18899 }
18900 if received.len() >= expected.len() {
18901 break;
18902 }
18903 std::thread::yield_now();
18904 }
18905 assert_eq!(
18906 received, expected,
18907 "the peer must receive the WINDOW_UPDATE, then the PING ACK"
18908 );
18909 }
18910
18911 /// The RED-confirmed test above sends a HEADERS payload that is nothing
18912 /// but a field block: no `Pad Length`, no `Stream Dependency`/`Weight`.
18913 /// That shape cannot catch correction #1 — RFC 9113 §6.2 lays a HEADERS
18914 /// payload out as `[Pad Length?][Priority?][field block][padding?]`, and
18915 /// `refuse_stream_and_discard`'s new-stream callers pass the *whole*
18916 /// payload. A decode that forgets to strip the PADDED/PRIORITY prefix
18917 /// feeds the HPACK decoder six bytes of pad-length/dependency/weight
18918 /// garbage before the real field block starts.
18919 ///
18920 /// To SEE THIS RED: in [`decode_discarded_field_block`]'s
18921 /// `DiscardedFieldBlock::New` arm, replace the `let fragment = headers
18922 /// .header_block_fragment.data_opt(payload)...` lines with `let fragment
18923 /// = payload;` — i.e. hand the connection decoder the raw payload
18924 /// unstripped, the way a fix that only handled the un-padded case would.
18925 /// The peer's first block is still legitimate HPACK once you skip past
18926 /// the 6-byte prefix, but decoding from byte 0 instead makes the pad
18927 /// length byte and the first three dependency/weight octets look like
18928 /// HPACK opcodes. Verified 2026-09-21: the decode fails with
18929 /// `H2Error::CompressionError`, observed via the connection reaching
18930 /// `H2State::GoAway` (the panic is on the "connection usable" assertion,
18931 /// not the later decode assertion — the malformed prefix corrupts the
18932 /// connection before the probe even gets to send its second block).
18933 #[test]
18934 fn a_refused_padded_prioritized_stream_keeps_the_hpack_decoder_in_sync() {
18935 use std::io::Write;
18936
18937 let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 20, 16_384)));
18938 let (mut connection, mut peer) = test_h2_connection(&pool, None);
18939 let mut context = test_context(&pool);
18940 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
18941
18942 connection.core.state = H2State::Header;
18943 connection
18944 .core
18945 .stream_table
18946 .set_expect_read(Some((H2StreamId::Zero, 9)));
18947 connection.core.drain.__test_set_draining();
18948
18949 // Same probe technique as the sibling test: this appends
18950 // `x-sozu-probe: alpha` to the peer's dynamic table at index 62.
18951 let mut peer_encoder = crate::protocol::mux::hpack::Encoder::new();
18952 let field_block = peer_encoder.encode([
18953 (&b":method"[..], &b"GET"[..]),
18954 (&b":scheme"[..], &b"https"[..]),
18955 (&b":authority"[..], &b"example.com"[..]),
18956 (&b":path"[..], &b"/refused"[..]),
18957 (&b"x-sozu-probe"[..], &b"alpha"[..]),
18958 ]);
18959
18960 // RFC 9113 §6.2 HEADERS payload: Pad Length (1) + Stream Dependency
18961 // (4, exclusive bit clear, depends on stream 0) + Weight (1) + the
18962 // field block + 3 zero padding bytes.
18963 const PAD_LEN: u8 = 3;
18964 let mut payload = Vec::with_capacity(1 + 5 + field_block.len() + PAD_LEN as usize);
18965 payload.push(PAD_LEN);
18966 payload.extend_from_slice(&0u32.to_be_bytes()); // Stream Dependency
18967 payload.push(15); // Weight
18968 payload.extend_from_slice(&field_block);
18969 payload.extend(std::iter::repeat_n(0u8, PAD_LEN as usize));
18970
18971 // HEADERS, END_STREAM | END_HEADERS | PADDED | PRIORITY, stream 1.
18972 let mut frame = Vec::with_capacity(9 + payload.len());
18973 frame.extend_from_slice(&(payload.len() as u32).to_be_bytes()[1..]);
18974 frame.push(1);
18975 frame.push(
18976 parser::FLAG_END_STREAM
18977 | parser::FLAG_END_HEADERS
18978 | parser::FLAG_PADDED
18979 | parser::FLAG_PRIORITY,
18980 );
18981 frame.extend_from_slice(&1u32.to_be_bytes());
18982 frame.extend_from_slice(&payload);
18983 peer.write_all(&frame)
18984 .expect("loopback write must complete");
18985 peer.flush().expect("loopback flush must complete");
18986
18987 for _ in 0..64 {
18988 connection.readable(&mut context, EndpointClient(&mut router));
18989 if matches!(connection.core.state, H2State::Header)
18990 && matches!(
18991 connection.core.stream_table.expect_read(),
18992 Some((H2StreamId::Zero, 9))
18993 )
18994 && !connection.core.control_tx.pending().is_empty()
18995 {
18996 break;
18997 }
18998 std::thread::yield_now();
18999 }
19000
19001 assert_eq!(
19002 connection.core.control_tx.pending(),
19003 vec![(1, H2Error::RefusedStream)],
19004 "the drain gate must refuse stream 1 with RST_STREAM(REFUSED_STREAM)"
19005 );
19006 assert!(
19007 connection.core.stream_table.is_empty(),
19008 "a refused stream must not be created"
19009 );
19010 assert!(
19011 !matches!(connection.core.state, H2State::Error | H2State::GoAway),
19012 "refusing a PADDED|PRIORITY HEADERS frame must leave the connection \
19013 usable: {:?}",
19014 connection.core.state
19015 );
19016
19017 let next_block = peer_encoder.encode([(&b"x-sozu-probe"[..], &b"alpha"[..])]);
19018 assert_eq!(
19019 next_block.len(),
19020 1,
19021 "the peer must now reference its dynamic entry, not re-send a literal"
19022 );
19023
19024 let mut decoded = Vec::new();
19025 let status = connection
19026 .core
19027 .hpack
19028 .decoder_mut()
19029 .decode_with_cb(&next_block, |k, v| {
19030 decoded.push((k.into_owned(), v.into_owned()));
19031 });
19032
19033 assert!(
19034 status.is_ok(),
19035 "the connection decoder must still resolve the peer's dynamic table \
19036 after a refused PADDED|PRIORITY stream, got {status:?}"
19037 );
19038 assert_eq!(
19039 decoded,
19040 vec![(b"x-sozu-probe".to_vec(), b"alpha".to_vec())],
19041 "the refused stream's field block — located past the Pad Length and \
19042 Priority prefix — must have been decoded into the connection's \
19043 HPACK context"
19044 );
19045 }
19046
19047 // ── `self.zero` is read-accumulation AND write scratch — that is the bug ──
19048 //
19049 // `ConnectionH2::zero` serves two roles: the read-side accumulation
19050 // buffer for a HEADERS/CONTINUATION field block in progress, and the
19051 // write scratch space `flush_pending_control_frames` reuses to
19052 // serialise ANY queued WINDOW_UPDATE or RST_STREAM — for any stream,
19053 // not just the one being reassembled. `mod.rs`'s inner event loop
19054 // dispatches frontend `readable()` then `writable()` in the same sweep,
19055 // so a legitimate, non-refused multi-frame header block can be
19056 // clobbered by completely unrelated connection-level flow-control
19057 // housekeeping before its CONTINUATION frame ever arrives — no
19058 // adversarial peer required.
19059 //
19060 // That was the pre-image of #1396/#1397/#1401 and, for the write
19061 // scratch, of #1604: the accumulator moved to `header_reassembly`, and
19062 // every control frame is now queued in the separate `output`. The tests
19063 // below keep pinning the outcome, a block that survives every
19064 // interleaved flush; their RED notes describe the tree they were written
19065 // against.
19066
19067 /// To SEE THIS RED: this is the pre-existing behaviour on `main`, no
19068 /// mutation needed. `flush_pending_control_frames`'s WINDOW_UPDATE-drain
19069 /// stage (`let kawa = &mut self.zero; kawa.storage.clear();`) runs
19070 /// unconditionally whenever a WINDOW_UPDATE is queued, with no check on
19071 /// `self.state`. Guarding that stage (and the RST_STREAM-drain stage
19072 /// beneath it) on `matches!(self.state, H2State::ContinuationHeader(_) |
19073 /// H2State::ContinuationFrame(_))` is exactly the fix this test proves.
19074 #[test]
19075 fn a_legitimate_continuation_survives_an_unrelated_window_update_flush() {
19076 use std::io::Write;
19077
19078 let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 20, 16_384)));
19079 let (mut connection, mut peer) = test_h2_connection(&pool, None);
19080 let mut context = test_context(&pool);
19081 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
19082
19083 // Past the preface/SETTINGS handshake, waiting on a frame header.
19084 // `drain.draining` stays false: stream 1 must be genuinely accepted,
19085 // not refused — this is the ordinary, non-refusal path.
19086 connection.core.state = H2State::Header;
19087 connection
19088 .core
19089 .stream_table
19090 .set_expect_read(Some((H2StreamId::Zero, 9)));
19091
19092 // The peer's encoder. The marker header's name is in neither table,
19093 // so encoding it appends `x-sozu-marker: legitimate-value` to the
19094 // peer's dynamic table at a known index — the same technique
19095 // `a_refused_stream_keeps_the_hpack_decoder_in_sync` uses to prove
19096 // decoder sync, here applied to a stream that is never refused.
19097 let mut peer_encoder = crate::protocol::mux::hpack::Encoder::new();
19098 let field_block = peer_encoder.encode([
19099 (&b":method"[..], &b"GET"[..]),
19100 (&b":scheme"[..], &b"https"[..]),
19101 (&b":authority"[..], &b"example.com"[..]),
19102 (&b":path"[..], &b"/legit-multiframe"[..]),
19103 (&b"x-sozu-marker"[..], &b"legitimate-value"[..]),
19104 ]);
19105 // Split well past the 13 bytes a single WINDOW_UPDATE frame occupies,
19106 // so the clobber this test provokes lands inside real field-block
19107 // bytes rather than in a margin this test invented.
19108 assert!(
19109 field_block.len() > 26,
19110 "the probe block must be large enough to split meaningfully"
19111 );
19112 let split = field_block.len() / 2;
19113 let (first_half, second_half) = field_block.split_at(split);
19114
19115 // HEADERS, END_STREAM but NOT END_HEADERS, stream 1: a legitimate
19116 // multi-frame header block, exactly as a large request produces.
19117 let mut headers_frame = Vec::with_capacity(9 + first_half.len());
19118 headers_frame.extend_from_slice(&(first_half.len() as u32).to_be_bytes()[1..]);
19119 headers_frame.push(1); // HEADERS
19120 headers_frame.push(parser::FLAG_END_STREAM);
19121 headers_frame.extend_from_slice(&1u32.to_be_bytes());
19122 headers_frame.extend_from_slice(first_half);
19123 peer.write_all(&headers_frame)
19124 .expect("loopback write must complete");
19125 peer.flush().expect("loopback flush must complete");
19126
19127 // Drive reads until the connection is genuinely waiting on the
19128 // CONTINUATION frame for stream 1 — accepted, not refused.
19129 for _ in 0..64 {
19130 connection.readable(&mut context, EndpointClient(&mut router));
19131 if matches!(connection.core.state, H2State::ContinuationHeader(_)) {
19132 break;
19133 }
19134 std::thread::yield_now();
19135 }
19136 assert!(
19137 matches!(connection.core.state, H2State::ContinuationHeader(_)),
19138 "stream 1's HEADERS frame must be accepted and await CONTINUATION: {:?}",
19139 connection.core.state
19140 );
19141 assert!(
19142 !connection.core.stream_table.is_empty(),
19143 "a non-refused HEADERS frame must create stream 1"
19144 );
19145
19146 // Completely unrelated connection-level housekeeping: replenishing
19147 // the connection flow-control window after DATA consumed on some
19148 // other stream. Nothing about it targets stream 1 or its
19149 // in-progress header block — this is the ordinary path, not an
19150 // attack.
19151 connection.core.queue_window_update(0, 65_535);
19152 connection.writable(&mut context, EndpointClient(&mut router));
19153
19154 // Now the CONTINUATION frame completing the block arrives.
19155 let mut continuation_frame = Vec::with_capacity(9 + second_half.len());
19156 continuation_frame.extend_from_slice(&(second_half.len() as u32).to_be_bytes()[1..]);
19157 continuation_frame.push(9); // CONTINUATION
19158 continuation_frame.push(parser::FLAG_END_HEADERS);
19159 continuation_frame.extend_from_slice(&1u32.to_be_bytes());
19160 continuation_frame.extend_from_slice(second_half);
19161 peer.write_all(&continuation_frame)
19162 .expect("loopback write must complete");
19163 peer.flush().expect("loopback flush must complete");
19164
19165 for _ in 0..64 {
19166 connection.readable(&mut context, EndpointClient(&mut router));
19167 if !matches!(
19168 connection.core.state,
19169 H2State::ContinuationHeader(_) | H2State::ContinuationFrame(_)
19170 ) {
19171 break;
19172 }
19173 std::thread::yield_now();
19174 }
19175
19176 assert!(
19177 !matches!(connection.core.state, H2State::Error | H2State::GoAway),
19178 "an unrelated WINDOW_UPDATE flush must not corrupt a legitimate \
19179 in-progress header block into a connection error: {:?}",
19180 connection.core.state
19181 );
19182
19183 // The peer's next block references the dynamic entry its encoder
19184 // added while encoding stream 1's request.
19185 let next_block = peer_encoder.encode([(&b"x-sozu-marker"[..], &b"legitimate-value"[..])]);
19186 assert_eq!(
19187 next_block.len(),
19188 1,
19189 "the peer must now reference its dynamic entry, not re-send a literal"
19190 );
19191
19192 let mut decoded = Vec::new();
19193 let status = connection
19194 .core
19195 .hpack
19196 .decoder_mut()
19197 .decode_with_cb(&next_block, |k, v| {
19198 decoded.push((k.into_owned(), v.into_owned()));
19199 });
19200
19201 assert!(
19202 status.is_ok(),
19203 "the connection decoder must still resolve the peer's dynamic table \
19204 after an unrelated WINDOW_UPDATE flush during header-block \
19205 reassembly, got {status:?}"
19206 );
19207 assert_eq!(
19208 decoded,
19209 vec![(b"x-sozu-marker".to_vec(), b"legitimate-value".to_vec())],
19210 "stream 1's field block must have been decoded byte-for-byte, not \
19211 clobbered by the unrelated WINDOW_UPDATE flush"
19212 );
19213 }
19214
19215 /// Same shape as
19216 /// `a_legitimate_continuation_survives_an_unrelated_window_update_flush`,
19217 /// but the clobbering trigger is `graceful_goaway` itself instead of an
19218 /// unrelated WINDOW_UPDATE flush — the third instance of the same bug
19219 /// class (issue #1398).
19220 ///
19221 /// Since #1637 the advisory GOAWAY is no longer deferred while the block
19222 /// reassembles: it is queued in `self.output` and flushed in the same
19223 /// pass, and this test checks both halves on the wire — the peer reads
19224 /// the 17-byte `GOAWAY(NO_ERROR, STREAM_ID_MAX)` BEFORE it sends the
19225 /// CONTINUATION, and the block that CONTINUATION completes still decodes
19226 /// byte-for-byte afterwards.
19227 ///
19228 /// To SEE THIS RED, either reintroduce the #1401 deferral (hold the
19229 /// GOAWAY back while `H2State::ContinuationHeader` holds) — the
19230 /// `read_exact` of the advisory GOAWAY before the CONTINUATION finds nothing
19231 /// — or serialize the GOAWAY into `self.zero.storage` as the pre-#1604
19232 /// code did, which corrupts the block and fails the decode assertions.
19233 #[test]
19234 fn a_legitimate_continuation_survives_a_graceful_goaway() {
19235 use std::io::{Read, Write};
19236
19237 let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 20, 16_384)));
19238 let (mut connection, mut peer) = test_h2_connection(&pool, None);
19239 let mut context = test_context(&pool);
19240 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
19241
19242 // Past the preface/SETTINGS handshake, waiting on a frame header.
19243 // `drain.draining` stays false until `graceful_goaway` is called
19244 // below: stream 1 must be genuinely accepted, not refused — this is
19245 // the ordinary, non-refusal path.
19246 connection.core.state = H2State::Header;
19247 connection
19248 .core
19249 .stream_table
19250 .set_expect_read(Some((H2StreamId::Zero, 9)));
19251
19252 // The peer's encoder. The marker header's name is in neither table,
19253 // so encoding it appends `x-sozu-marker: legitimate-value` to the
19254 // peer's dynamic table at a known index — the same technique
19255 // `a_legitimate_continuation_survives_an_unrelated_window_update_flush`
19256 // uses to prove decoder sync.
19257 let mut peer_encoder = crate::protocol::mux::hpack::Encoder::new();
19258 let field_block = peer_encoder.encode([
19259 (&b":method"[..], &b"GET"[..]),
19260 (&b":scheme"[..], &b"https"[..]),
19261 (&b":authority"[..], &b"example.com"[..]),
19262 (&b":path"[..], &b"/legit-multiframe"[..]),
19263 (&b"x-sozu-marker"[..], &b"legitimate-value"[..]),
19264 ]);
19265 // Same header set and split proportions as
19266 // `a_legitimate_continuation_survives_an_unrelated_window_update_flush`:
19267 // `header_block_fragment`'s window is a `(start, len)` pair recorded
19268 // once, independent of `self.zero.storage`'s own bookkeeping, so
19269 // *any* clear-then-refill of that buffer — 13 bytes of WINDOW_UPDATE
19270 // or 17 bytes of GOAWAY, it makes no difference — leaves the window
19271 // pointing at the same wrong offset once `end`/`head` reset to 0 and
19272 // the CONTINUATION's own bytes land there instead. Matching the split
19273 // reproduces the same decodable-but-wrong HPACK byte sequence rather
19274 // than a split that happens to land on an invalid opcode.
19275 assert!(
19276 field_block.len() > 34,
19277 "the probe block must be large enough to split meaningfully"
19278 );
19279 let split = field_block.len() / 2;
19280 let (first_half, second_half) = field_block.split_at(split);
19281
19282 // HEADERS, END_STREAM but NOT END_HEADERS, stream 1: a legitimate
19283 // multi-frame header block, exactly as a large request produces.
19284 let mut headers_frame = Vec::with_capacity(9 + first_half.len());
19285 headers_frame.extend_from_slice(&(first_half.len() as u32).to_be_bytes()[1..]);
19286 headers_frame.push(1); // HEADERS
19287 headers_frame.push(parser::FLAG_END_STREAM);
19288 headers_frame.extend_from_slice(&1u32.to_be_bytes());
19289 headers_frame.extend_from_slice(first_half);
19290 peer.write_all(&headers_frame)
19291 .expect("loopback write must complete");
19292 peer.flush().expect("loopback flush must complete");
19293
19294 // Drive reads until the connection is genuinely waiting on the
19295 // CONTINUATION frame for stream 1 — accepted, not refused.
19296 for _ in 0..64 {
19297 connection.readable(&mut context, EndpointClient(&mut router));
19298 if matches!(connection.core.state, H2State::ContinuationHeader(_)) {
19299 break;
19300 }
19301 std::thread::yield_now();
19302 }
19303 assert!(
19304 matches!(connection.core.state, H2State::ContinuationHeader(_)),
19305 "stream 1's HEADERS frame must be accepted and await CONTINUATION: {:?}",
19306 connection.core.state
19307 );
19308 assert!(
19309 !connection.core.stream_table.is_empty(),
19310 "a non-refused HEADERS frame must create stream 1"
19311 );
19312
19313 // The proxy decides to drain — a graceful shutdown or hot reload
19314 // landing mid-reassembly, exactly as issue #1398 describes. Nothing
19315 // about it targets stream 1 or its in-progress header block.
19316 let drain_at = connection.core.now;
19317 let result = connection.graceful_goaway(drain_at);
19318 assert!(
19319 matches!(result, MuxResult::Continue),
19320 "graceful_goaway must not tear the connection down mid-reassembly: {result:?}"
19321 );
19322
19323 // The forced-close budget must arm from the moment the proxy decided
19324 // to drain — `Mux::shutting_down` samples exactly this field against
19325 // `graceful_shutdown_deadline`.
19326 assert!(
19327 connection.core.drain.draining(),
19328 "graceful_goaway must mark the connection as draining immediately"
19329 );
19330 assert_eq!(
19331 connection.core.drain.__test_started_at(),
19332 Some(drain_at),
19333 "the forced-close budget must arm from `graceful_goaway`'s `now` parameter"
19334 );
19335 assert!(
19336 matches!(connection.core.state, H2State::ContinuationHeader(_)),
19337 "queueing the advisory GOAWAY must leave the reassembly state alone: {:?}",
19338 connection.core.state
19339 );
19340
19341 // Mirror `Mux::shutting_down_inner` exactly: it calls
19342 // `flush_output_buffer()` immediately after `graceful_goaway()`
19343 // returns `Continue`, because edge-triggered epoll won't deliver a
19344 // fresh WRITABLE event for an already-writable socket. The advisory
19345 // GOAWAY leaves in this flush, while the block is still incomplete.
19346 connection.flush_output_buffer();
19347 assert!(
19348 connection.core.output.is_empty(),
19349 "the advisory GOAWAY must have been fully flushed to the socket"
19350 );
19351
19352 let mut expected = [0u8; 17];
19353 let (_, expected_size) =
19354 serializer::gen_goaway(&mut expected, STREAM_ID_MAX, H2Error::NoError)
19355 .expect("serializing the expected GOAWAY must succeed");
19356 assert_eq!(expected_size, 17);
19357
19358 let mut received = [0u8; 17];
19359 peer.set_read_timeout(Some(Duration::from_secs(5)))
19360 .expect("setting a read timeout must succeed");
19361 peer.read_exact(&mut received)
19362 .expect("the peer must receive the advisory GOAWAY before it completes the block");
19363 assert_eq!(
19364 received, expected,
19365 "the advisory GOAWAY must reach the peer byte-for-byte while its \
19366 header block is still incomplete"
19367 );
19368
19369 // Now the CONTINUATION frame completing the block arrives.
19370 let mut continuation_frame = Vec::with_capacity(9 + second_half.len());
19371 continuation_frame.extend_from_slice(&(second_half.len() as u32).to_be_bytes()[1..]);
19372 continuation_frame.push(9); // CONTINUATION
19373 continuation_frame.push(parser::FLAG_END_HEADERS);
19374 continuation_frame.extend_from_slice(&1u32.to_be_bytes());
19375 continuation_frame.extend_from_slice(second_half);
19376 peer.write_all(&continuation_frame)
19377 .expect("loopback write must complete");
19378 peer.flush().expect("loopback flush must complete");
19379
19380 for _ in 0..64 {
19381 connection.readable(&mut context, EndpointClient(&mut router));
19382 if !matches!(
19383 connection.core.state,
19384 H2State::ContinuationHeader(_) | H2State::ContinuationFrame(_)
19385 ) {
19386 break;
19387 }
19388 std::thread::yield_now();
19389 }
19390
19391 assert!(
19392 !matches!(connection.core.state, H2State::Error | H2State::GoAway),
19393 "a graceful_goaway landing mid-reassembly must not corrupt a \
19394 legitimate in-progress header block into a connection error: {:?}",
19395 connection.core.state
19396 );
19397
19398 // The peer's next block references the dynamic entry its encoder
19399 // added while encoding stream 1's request.
19400 let next_block = peer_encoder.encode([(&b"x-sozu-marker"[..], &b"legitimate-value"[..])]);
19401 assert_eq!(
19402 next_block.len(),
19403 1,
19404 "the peer must now reference its dynamic entry, not re-send a literal"
19405 );
19406
19407 let mut decoded = Vec::new();
19408 let status = connection
19409 .core
19410 .hpack
19411 .decoder_mut()
19412 .decode_with_cb(&next_block, |k, v| {
19413 decoded.push((k.into_owned(), v.into_owned()));
19414 });
19415
19416 assert!(
19417 status.is_ok(),
19418 "the connection decoder must still resolve the peer's dynamic table \
19419 after a graceful_goaway during header-block reassembly, got {status:?}"
19420 );
19421 assert_eq!(
19422 decoded,
19423 vec![(b"x-sozu-marker".to_vec(), b"legitimate-value".to_vec())],
19424 "stream 1's field block must have been decoded byte-for-byte, not \
19425 clobbered by graceful_goaway"
19426 );
19427
19428 // Nothing else was owed to the peer: the drain is not a second
19429 // GOAWAY, and the completed block leaves nothing queued.
19430 assert!(
19431 connection.core.output.is_empty(),
19432 "completing the block must not queue a second advisory GOAWAY"
19433 );
19434 }
19435
19436 /// Regression test for #1423: `flush_pending_control_frames`'s
19437 /// `frontend_hung_up_while_draining()` stage cleared `zero.storage`
19438 /// unconditionally, with no `header_block_reassembly_in_progress()`
19439 /// guard — unlike its three sibling stages, which #1397 and #1401
19440 /// already gated. LIFECYCLE.md invariant 24 tracked this as a "known
19441 /// live gap, reported and NOT fixed" until this changeset.
19442 ///
19443 /// Same shape as `a_legitimate_continuation_survives_a_graceful_goaway`,
19444 /// but the clobbering trigger is a HUP/ERROR readiness event landing
19445 /// while draining — exactly `frontend_hung_up_while_draining()`'s own
19446 /// condition — instead of `graceful_goaway` itself. This test drives
19447 /// the gap BETWEEN two frames (a HUP arriving while
19448 /// `expect_read = Some((Zero, 9))`, waiting for the next CONTINUATION's
19449 /// header, with nothing of this block's history left in `zero.storage`
19450 /// to lose). Moving the accumulator out of `zero.storage` into
19451 /// `self.header_reassembly` (`h2_header_reassembly.rs`) makes THIS
19452 /// scenario safe regardless of whether this stage is guarded: there is
19453 /// no accumulated history left in `zero.storage` for it to clobber.
19454 ///
19455 /// That is NOT the whole of #1423, though — see
19456 /// `a_continuation_frame_split_by_tcp_segmentation_survives_a_hup_while_draining`
19457 /// below for the narrower "HUP lands mid-frame, not between frames"
19458 /// case this test does not cover, which review of this changeset's
19459 /// first version (`e1c3c2fb`) proved the accumulator move alone does
19460 /// NOT make safe, and which is why `frontend_hung_up_while_draining()`'s
19461 /// stage carries the same `header_block_reassembly_in_progress()` guard
19462 /// as its three siblings after all.
19463 ///
19464 /// To SEE THIS RED on this step's base commit (`f76c8eb3`, before the
19465 /// accumulator moved out of `zero.storage`): run this test unmodified
19466 /// against that commit. `flush_pending_control_frames`'s first stage
19467 /// clears `zero.storage` — which, on that commit, still holds stream 1's
19468 /// entire accumulated first-half fragment — before the CONTINUATION
19469 /// frame completing the block arrives; the CONTINUATION's own bytes
19470 /// then land at the wrong (reset-to-zero) offset, and the connection's
19471 /// HPACK decoder desyncs from the peer's encoder. The failure is
19472 /// silent corruption, not a crash (HPACK carries no self-check): the
19473 /// `decode_with_cb` assertion below fails with a decode error, or
19474 /// succeeds but yields something other than
19475 /// `[(b"x-sozu-marker", b"legitimate-value")]`.
19476 #[test]
19477 fn a_legitimate_continuation_survives_a_hup_while_draining() {
19478 use std::io::Write;
19479
19480 let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 20, 16_384)));
19481 let (mut connection, mut peer) = test_h2_connection(&pool, None);
19482 let mut context = test_context(&pool);
19483 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
19484
19485 // Past the preface/SETTINGS handshake, waiting on a frame header.
19486 // `drain.draining` stays false until manually forced below: stream 1
19487 // must be genuinely accepted, not refused — this is the ordinary,
19488 // non-refusal path.
19489 connection.core.state = H2State::Header;
19490 connection
19491 .core
19492 .stream_table
19493 .set_expect_read(Some((H2StreamId::Zero, 9)));
19494
19495 // The peer's encoder. The marker header's name is in neither table,
19496 // so encoding it appends `x-sozu-marker: legitimate-value` to the
19497 // peer's dynamic table at a known index — the same technique the
19498 // sibling WINDOW_UPDATE/graceful_goaway tests use to prove decoder
19499 // sync.
19500 let mut peer_encoder = crate::protocol::mux::hpack::Encoder::new();
19501 let field_block = peer_encoder.encode([
19502 (&b":method"[..], &b"GET"[..]),
19503 (&b":scheme"[..], &b"https"[..]),
19504 (&b":authority"[..], &b"example.com"[..]),
19505 (&b":path"[..], &b"/legit-multiframe"[..]),
19506 (&b"x-sozu-marker"[..], &b"legitimate-value"[..]),
19507 ]);
19508 assert!(
19509 field_block.len() > 34,
19510 "the probe block must be large enough to split meaningfully"
19511 );
19512 let split = field_block.len() / 2;
19513 let (first_half, second_half) = field_block.split_at(split);
19514
19515 // HEADERS, END_STREAM but NOT END_HEADERS, stream 1: a legitimate
19516 // multi-frame header block, exactly as a large request produces.
19517 let mut headers_frame = Vec::with_capacity(9 + first_half.len());
19518 headers_frame.extend_from_slice(&(first_half.len() as u32).to_be_bytes()[1..]);
19519 headers_frame.push(1); // HEADERS
19520 headers_frame.push(parser::FLAG_END_STREAM);
19521 headers_frame.extend_from_slice(&1u32.to_be_bytes());
19522 headers_frame.extend_from_slice(first_half);
19523 peer.write_all(&headers_frame)
19524 .expect("loopback write must complete");
19525 peer.flush().expect("loopback flush must complete");
19526
19527 // Drive reads until the connection is genuinely waiting on the
19528 // CONTINUATION frame for stream 1 — accepted, not refused.
19529 for _ in 0..64 {
19530 connection.readable(&mut context, EndpointClient(&mut router));
19531 if matches!(connection.core.state, H2State::ContinuationHeader(_)) {
19532 break;
19533 }
19534 std::thread::yield_now();
19535 }
19536 assert!(
19537 matches!(connection.core.state, H2State::ContinuationHeader(_)),
19538 "stream 1's HEADERS frame must be accepted and await CONTINUATION: {:?}",
19539 connection.core.state
19540 );
19541 assert!(
19542 !connection.core.stream_table.is_empty(),
19543 "a non-refused HEADERS frame must create stream 1"
19544 );
19545
19546 // Exactly #1423's trigger: the connection is draining AND the
19547 // frontend readiness reports HUP, landing squarely mid-reassembly.
19548 // A peer can legitimately produce this by sending TCP FIN together
19549 // with (or shortly after) the last CONTINUATION frame it manages to
19550 // get out, observed by edge-triggered epoll as one combined event.
19551 connection.core.drain.__test_set_draining();
19552 connection.core.readiness.event.insert(Ready::HUP);
19553
19554 // `flush_pending_control_frames`'s first, HUP-gated stage fires
19555 // here.
19556 connection.writable(&mut context, EndpointClient(&mut router));
19557
19558 // Real epoll delivers HUP once; un-signal it so the reads below
19559 // reflect the peer's normal traffic rather than a second synthetic
19560 // HUP this test injected by hand.
19561 connection.core.readiness.event.remove(Ready::HUP);
19562
19563 // Now the CONTINUATION frame completing the block arrives — still
19564 // fully deliverable, exactly as it would be if the peer's FIN and
19565 // its last frame arrived close together but the block itself was
19566 // sent in full.
19567 let mut continuation_frame = Vec::with_capacity(9 + second_half.len());
19568 continuation_frame.extend_from_slice(&(second_half.len() as u32).to_be_bytes()[1..]);
19569 continuation_frame.push(9); // CONTINUATION
19570 continuation_frame.push(parser::FLAG_END_HEADERS);
19571 continuation_frame.extend_from_slice(&1u32.to_be_bytes());
19572 continuation_frame.extend_from_slice(second_half);
19573 peer.write_all(&continuation_frame)
19574 .expect("loopback write must complete");
19575 peer.flush().expect("loopback flush must complete");
19576
19577 for _ in 0..64 {
19578 connection.readable(&mut context, EndpointClient(&mut router));
19579 if !matches!(
19580 connection.core.state,
19581 H2State::ContinuationHeader(_) | H2State::ContinuationFrame(_)
19582 ) {
19583 break;
19584 }
19585 std::thread::yield_now();
19586 }
19587
19588 assert!(
19589 !matches!(connection.core.state, H2State::Error | H2State::GoAway),
19590 "a HUP-while-draining event landing mid-reassembly must not corrupt \
19591 a legitimate in-progress header block into a connection error: {:?}",
19592 connection.core.state
19593 );
19594
19595 // The peer's next block references the dynamic entry its encoder
19596 // added while encoding stream 1's request.
19597 let next_block = peer_encoder.encode([(&b"x-sozu-marker"[..], &b"legitimate-value"[..])]);
19598 assert_eq!(
19599 next_block.len(),
19600 1,
19601 "the peer must now reference its dynamic entry, not re-send a literal"
19602 );
19603
19604 let mut decoded = Vec::new();
19605 let status = connection
19606 .core
19607 .hpack
19608 .decoder_mut()
19609 .decode_with_cb(&next_block, |k, v| {
19610 decoded.push((k.into_owned(), v.into_owned()));
19611 });
19612
19613 assert!(
19614 status.is_ok(),
19615 "the connection decoder must still resolve the peer's dynamic table \
19616 after a HUP-while-draining event during header-block reassembly, \
19617 got {status:?}"
19618 );
19619 assert_eq!(
19620 decoded,
19621 vec![(b"x-sozu-marker".to_vec(), b"legitimate-value".to_vec())],
19622 "stream 1's field block must have been decoded byte-for-byte, not \
19623 clobbered by the HUP-while-draining stage of \
19624 flush_pending_control_frames"
19625 );
19626 }
19627
19628 /// Regression test for B1 (review of `e1c3c2fb`): `handle_headers_frame`'s
19629 /// RFC 7540 §5.3.1 self-dependency early return — `reset_stream` +
19630 /// `remove_dead_stream` when a stream's own PRIORITY depends on itself —
19631 /// used to skip retiring `self.header_reassembly` before returning. When
19632 /// the aborted stream's block had gone through CONTINUATION reassembly
19633 /// (`header_reassembly.is_in_progress() == true` at abort time), the
19634 /// flag leaked into the very NEXT HEADERS frame processed on this
19635 /// connection: that frame's own "read from `zero.storage`" fast path
19636 /// was silently skipped in favour of the aborted stream's stale,
19637 /// already-discarded bytes — corrupting an entirely unrelated stream's
19638 /// decoded request. This reproduces in BOTH debug and release builds:
19639 /// nothing on this path was ever a `debug_assert!`.
19640 ///
19641 /// To SEE THIS RED on `e1c3c2fb` (before this fix): stream 1's block
19642 /// completes reassembly, its RFC 7540 PRIORITY self-dependency
19643 /// (`stream_dependency.stream_id == 1`) aborts it via `reset_stream`
19644 /// without retiring the accumulator, and
19645 /// `header_reassembly.is_in_progress()` stays `true`. Stream 3's own,
19646 /// unrelated, single-frame request is then decoded from stream 1's
19647 /// stale fragment instead of its own bytes: the `:path` pseudo-header
19648 /// this test asserts on is corrupted rather than
19649 /// `"/stream-3-legitimate"`.
19650 #[test]
19651 fn a_priority_self_dependency_reset_does_not_leak_the_reassembly_accumulator() {
19652 use std::io::Write;
19653
19654 let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 20, 16_384)));
19655 let (mut connection, mut peer) = test_h2_connection(&pool, None);
19656 let mut context = test_context(&pool);
19657 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
19658
19659 connection.core.state = H2State::Header;
19660 connection
19661 .core
19662 .stream_table
19663 .set_expect_read(Some((H2StreamId::Zero, 9)));
19664
19665 // Stream 1: a legitimate multi-frame header block — split across
19666 // HEADERS + CONTINUATION, exactly like the sibling reassembly tests
19667 // — but the FIRST frame also carries an RFC 7540 PRIORITY field
19668 // whose stream dependency is stream 1 itself (RFC 7540 §5.3.1: a
19669 // stream cannot depend on itself).
19670 let mut peer_encoder = crate::protocol::mux::hpack::Encoder::new();
19671 let field_block = peer_encoder.encode([
19672 (&b":method"[..], &b"GET"[..]),
19673 (&b":scheme"[..], &b"https"[..]),
19674 (&b":authority"[..], &b"example.com"[..]),
19675 (&b":path"[..], &b"/stream-1-aborted"[..]),
19676 ]);
19677 assert!(
19678 field_block.len() > 10,
19679 "the probe block must be large enough to split meaningfully"
19680 );
19681 let split = field_block.len() / 2;
19682 let (first_half, second_half) = field_block.split_at(split);
19683
19684 // HEADERS, PRIORITY (self-dependency), NOT END_HEADERS.
19685 let mut priority_prefix = Vec::with_capacity(5);
19686 priority_prefix.extend_from_slice(&1u32.to_be_bytes()); // stream_dependency = 1 (self)
19687 priority_prefix.push(15); // weight
19688 let mut headers_frame = Vec::with_capacity(9 + priority_prefix.len() + first_half.len());
19689 headers_frame.extend_from_slice(
19690 &((priority_prefix.len() + first_half.len()) as u32).to_be_bytes()[1..],
19691 );
19692 headers_frame.push(1); // HEADERS
19693 headers_frame.push(parser::FLAG_PRIORITY);
19694 headers_frame.extend_from_slice(&1u32.to_be_bytes());
19695 headers_frame.extend_from_slice(&priority_prefix);
19696 headers_frame.extend_from_slice(first_half);
19697 peer.write_all(&headers_frame)
19698 .expect("loopback write must complete");
19699 peer.flush().expect("loopback flush must complete");
19700
19701 for _ in 0..64 {
19702 connection.readable(&mut context, EndpointClient(&mut router));
19703 if matches!(connection.core.state, H2State::ContinuationHeader(_)) {
19704 break;
19705 }
19706 std::thread::yield_now();
19707 }
19708 assert!(
19709 matches!(connection.core.state, H2State::ContinuationHeader(_)),
19710 "stream 1's HEADERS frame must be accepted and await CONTINUATION: {:?}",
19711 connection.core.state
19712 );
19713
19714 // Complete the block — this re-enters `handle_headers_frame`, which
19715 // detects the self-dependency and aborts stream 1.
19716 let mut continuation_frame = Vec::with_capacity(9 + second_half.len());
19717 continuation_frame.extend_from_slice(&(second_half.len() as u32).to_be_bytes()[1..]);
19718 continuation_frame.push(9); // CONTINUATION
19719 continuation_frame.push(parser::FLAG_END_HEADERS);
19720 continuation_frame.extend_from_slice(&1u32.to_be_bytes());
19721 continuation_frame.extend_from_slice(second_half);
19722 peer.write_all(&continuation_frame)
19723 .expect("loopback write must complete");
19724 peer.flush().expect("loopback flush must complete");
19725
19726 for _ in 0..64 {
19727 connection.readable(&mut context, EndpointClient(&mut router));
19728 if connection.core.stream_table.get(1).is_none() {
19729 break;
19730 }
19731 std::thread::yield_now();
19732 }
19733
19734 // Premise: stream 1 really was torn down for self-dependency.
19735 assert!(
19736 connection.core.stream_table.get(1).is_none(),
19737 "the self-dependent stream must have been torn down"
19738 );
19739
19740 // The direct proof of the fix: the accumulator must be idle again
19741 // before any other stream's HEADERS frame is processed. `h2.rs`'s
19742 // own test module can reach the private field directly — no
19743 // test-only accessor needed.
19744 assert!(
19745 !connection.core.header_reassembly.is_in_progress(),
19746 "aborting a self-dependent stream mid-reassembly must retire the \
19747 accumulator, not leave it `is_in_progress() == true` for the \
19748 next HEADERS frame on this connection"
19749 );
19750
19751 // Stream 3: an entirely unrelated, single-frame, complete request.
19752 let stream3_block = peer_encoder.encode([
19753 (&b":method"[..], &b"GET"[..]),
19754 (&b":scheme"[..], &b"https"[..]),
19755 (&b":authority"[..], &b"example.com"[..]),
19756 (&b":path"[..], &b"/stream-3-legitimate"[..]),
19757 ]);
19758 let mut stream3_frame = Vec::with_capacity(9 + stream3_block.len());
19759 stream3_frame.extend_from_slice(&(stream3_block.len() as u32).to_be_bytes()[1..]);
19760 stream3_frame.push(1); // HEADERS
19761 stream3_frame.push(parser::FLAG_END_STREAM | parser::FLAG_END_HEADERS);
19762 stream3_frame.extend_from_slice(&3u32.to_be_bytes());
19763 stream3_frame.extend_from_slice(&stream3_block);
19764 peer.write_all(&stream3_frame)
19765 .expect("loopback write must complete");
19766 peer.flush().expect("loopback flush must complete");
19767
19768 // Wait until the connection is back at `Header`, waiting on the
19769 // NEXT frame's 9-byte header — not merely until the stream slot
19770 // exists, which `create_stream` populates as soon as the frame
19771 // HEADER is parsed, well before its payload is read and decoded.
19772 for _ in 0..64 {
19773 connection.readable(&mut context, EndpointClient(&mut router));
19774 if matches!(connection.core.state, H2State::Header)
19775 && matches!(
19776 connection.core.stream_table.expect_read(),
19777 Some((H2StreamId::Zero, 9))
19778 )
19779 {
19780 break;
19781 }
19782 std::thread::yield_now();
19783 }
19784
19785 let stream3_gid = connection
19786 .core
19787 .stream_table
19788 .get(3)
19789 .expect("stream 3's HEADERS frame must create a stream");
19790 assert_eq!(
19791 context.streams[stream3_gid].context.path.as_deref(),
19792 Some("/stream-3-legitimate"),
19793 "stream 3 must decode its OWN bytes, not stream 1's stale, \
19794 aborted reassembly fragment"
19795 );
19796 }
19797
19798 /// Regression test for B2 (review of `e1c3c2fb`): `flush_pending_control_frames`'s
19799 /// `frontend_hung_up_while_draining()` stage used to clear `zero.storage`
19800 /// unconditionally, on the premise that `Ready::HUP` means "no further
19801 /// bytes can ever arrive". That premise is false: `Ready::HUP` is
19802 /// `is_read_closed() || is_write_closed()` (`command/src/ready.rs`), and
19803 /// mio documents `is_read_closed()` as also true on a TCP half-close —
19804 /// a FIN with data the peer already sent still sitting, unread, in the
19805 /// kernel receive queue. A CONTINUATION frame's payload split across two
19806 /// TCP segments can have its FIRST segment already read into
19807 /// `zero.storage` (a genuine partial `socket_read()`, not stale bytes
19808 /// from a completed pass) when the readiness event batches HUP together
19809 /// with that first segment; `drive_frontend_shutdown_io` (`mod.rs`)
19810 /// force-calls `readable()` for H2 on every `shutting_down()` poll, so
19811 /// this is the ordinary soft-stop path, not a contrived corner case.
19812 ///
19813 /// To SEE THIS RED on `e1c3c2fb` (before this fix): the partial first
19814 /// segment is wiped by the unconditional clear; the second segment then
19815 /// lands at the wrong (reset-to-zero) offset once appended, and the
19816 /// connection's HPACK decoder desyncs from the peer's encoder — the
19817 /// same silent-wrong-values failure mode as #1397/#1401/#1423.
19818 #[test]
19819 fn a_continuation_frame_split_by_tcp_segmentation_survives_a_hup_while_draining() {
19820 use std::io::Write;
19821
19822 let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 20, 16_384)));
19823 let (mut connection, mut peer) = test_h2_connection(&pool, None);
19824 let mut context = test_context(&pool);
19825 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
19826
19827 connection.core.state = H2State::Header;
19828 connection
19829 .core
19830 .stream_table
19831 .set_expect_read(Some((H2StreamId::Zero, 9)));
19832
19833 let mut peer_encoder = crate::protocol::mux::hpack::Encoder::new();
19834 let field_block = peer_encoder.encode([
19835 (&b":method"[..], &b"GET"[..]),
19836 (&b":scheme"[..], &b"https"[..]),
19837 (&b":authority"[..], &b"example.com"[..]),
19838 (&b":path"[..], &b"/tcp-segmented"[..]),
19839 (&b"x-sozu-marker"[..], &b"legitimate-value"[..]),
19840 ]);
19841 assert!(
19842 field_block.len() > 40,
19843 "the probe block must be large enough to split meaningfully"
19844 );
19845 let split = field_block.len() / 2;
19846 let (first_half, continuation_payload) = field_block.split_at(split);
19847 assert!(
19848 continuation_payload.len() > 10,
19849 "the CONTINUATION frame's own payload must be large enough to \
19850 split into two TCP segments"
19851 );
19852 let payload_split = continuation_payload.len() / 2;
19853 let (payload_segment_1, payload_segment_2) = continuation_payload.split_at(payload_split);
19854
19855 // HEADERS, END_STREAM but NOT END_HEADERS, stream 1.
19856 let mut headers_frame = Vec::with_capacity(9 + first_half.len());
19857 headers_frame.extend_from_slice(&(first_half.len() as u32).to_be_bytes()[1..]);
19858 headers_frame.push(1); // HEADERS
19859 headers_frame.push(parser::FLAG_END_STREAM);
19860 headers_frame.extend_from_slice(&1u32.to_be_bytes());
19861 headers_frame.extend_from_slice(first_half);
19862 peer.write_all(&headers_frame)
19863 .expect("loopback write must complete");
19864 peer.flush().expect("loopback flush must complete");
19865
19866 for _ in 0..64 {
19867 connection.readable(&mut context, EndpointClient(&mut router));
19868 if matches!(connection.core.state, H2State::ContinuationHeader(_)) {
19869 break;
19870 }
19871 std::thread::yield_now();
19872 }
19873 assert!(
19874 matches!(connection.core.state, H2State::ContinuationHeader(_)),
19875 "stream 1's HEADERS frame must be accepted and await CONTINUATION: {:?}",
19876 connection.core.state
19877 );
19878
19879 // The CONTINUATION frame header, announcing the FULL payload
19880 // length, followed by only its FIRST TCP segment.
19881 let mut continuation_header_and_segment_1 = Vec::with_capacity(9 + payload_segment_1.len());
19882 continuation_header_and_segment_1
19883 .extend_from_slice(&(continuation_payload.len() as u32).to_be_bytes()[1..]);
19884 continuation_header_and_segment_1.push(9); // CONTINUATION
19885 continuation_header_and_segment_1.push(parser::FLAG_END_HEADERS);
19886 continuation_header_and_segment_1.extend_from_slice(&1u32.to_be_bytes());
19887 continuation_header_and_segment_1.extend_from_slice(payload_segment_1);
19888 peer.write_all(&continuation_header_and_segment_1)
19889 .expect("loopback write must complete");
19890 peer.flush().expect("loopback flush must complete");
19891
19892 // Drive reads until the connection has consumed exactly the first
19893 // segment and is waiting on the rest of THIS SAME CONTINUATION
19894 // frame's payload — a genuine partial `socket_read()`, proven by
19895 // `expect_read` still wanting the remainder.
19896 for _ in 0..64 {
19897 connection.readable(&mut context, EndpointClient(&mut router));
19898 if matches!(connection.core.state, H2State::ContinuationFrame(_))
19899 && matches!(
19900 connection.core.stream_table.expect_read(),
19901 Some((H2StreamId::Zero, remaining)) if remaining == payload_segment_2.len()
19902 )
19903 {
19904 break;
19905 }
19906 std::thread::yield_now();
19907 }
19908 assert!(
19909 matches!(connection.core.state, H2State::ContinuationFrame(_)),
19910 "the CONTINUATION frame must be mid-payload, waiting on its \
19911 second TCP segment: {:?}",
19912 connection.core.state
19913 );
19914 assert_eq!(
19915 connection.core.stream_table.expect_read(),
19916 Some((H2StreamId::Zero, payload_segment_2.len())),
19917 "expect_read must want exactly the remaining bytes of this \
19918 frame's own payload — proof this is a genuine partial read, \
19919 not an inter-frame gap"
19920 );
19921
19922 // Exactly B2's trigger: the connection is draining AND the frontend
19923 // readiness reports HUP, landing squarely mid-payload-read — the
19924 // shape a TCP half-close produces (mio's `is_read_closed()` can be
19925 // true with data the peer already sent still unread).
19926 connection.core.drain.__test_set_draining();
19927 connection.core.readiness.event.insert(Ready::HUP);
19928 connection.writable(&mut context, EndpointClient(&mut router));
19929 connection.core.readiness.event.remove(Ready::HUP);
19930
19931 // Now the second TCP segment, completing the CONTINUATION frame's
19932 // payload, arrives.
19933 peer.write_all(payload_segment_2)
19934 .expect("loopback write must complete");
19935 peer.flush().expect("loopback flush must complete");
19936
19937 for _ in 0..64 {
19938 connection.readable(&mut context, EndpointClient(&mut router));
19939 if !matches!(
19940 connection.core.state,
19941 H2State::ContinuationHeader(_) | H2State::ContinuationFrame(_)
19942 ) {
19943 break;
19944 }
19945 std::thread::yield_now();
19946 }
19947
19948 assert!(
19949 !matches!(connection.core.state, H2State::Error | H2State::GoAway),
19950 "a HUP-while-draining event landing mid-TCP-segment must not \
19951 corrupt a legitimate in-progress header block into a \
19952 connection error: {:?}",
19953 connection.core.state
19954 );
19955
19956 let next_block = peer_encoder.encode([(&b"x-sozu-marker"[..], &b"legitimate-value"[..])]);
19957 assert_eq!(
19958 next_block.len(),
19959 1,
19960 "the peer must now reference its dynamic entry, not re-send a literal"
19961 );
19962
19963 let mut decoded = Vec::new();
19964 let status = connection
19965 .core
19966 .hpack
19967 .decoder_mut()
19968 .decode_with_cb(&next_block, |k, v| {
19969 decoded.push((k.into_owned(), v.into_owned()));
19970 });
19971
19972 assert!(
19973 status.is_ok(),
19974 "the connection decoder must still resolve the peer's dynamic \
19975 table after a HUP-while-draining event landing mid-TCP-segment, \
19976 got {status:?}"
19977 );
19978 assert_eq!(
19979 decoded,
19980 vec![(b"x-sozu-marker".to_vec(), b"legitimate-value".to_vec())],
19981 "stream 1's field block must have been decoded byte-for-byte, \
19982 not clobbered by a HUP-while-draining event landing between \
19983 two TCP segments of the same CONTINUATION frame"
19984 );
19985 }
19986
19987 // ── The HPACK encoder across one multi-stream write pass ────────────
19988 //
19989 // `converter.rs`'s own unit tests drive ONE `H2BlockConverter` over ONE
19990 // kawa. Nothing there can see a property that only exists across the
19991 // streams of a single `write_streams` pass, and those are precisely the
19992 // two the per-`kawa.prepare` converter scoping has to preserve: the
19993 // RFC 7541 §6.3 size-update prefix belongs to the first header block of
19994 // the pass and to no other, and every block of the pass is encoded
19995 // against the SAME encoder dynamic table.
19996
19997 /// Split a captured H2 byte stream into its HEADERS frame payloads,
19998 /// each paired with the stream it belongs to, in wire order. DATA and
19999 /// control frames are skipped; a truncated capture is a test bug and
20000 /// asserts rather than silently returning a short list.
20001 fn headers_blocks(wire: &[u8]) -> Vec<(StreamId, Vec<u8>)> {
20002 let mut blocks = Vec::new();
20003 let mut offset = 0usize;
20004 while offset + parser::FRAME_HEADER_SIZE <= wire.len() {
20005 let payload_len = ((wire[offset] as usize) << 16)
20006 | ((wire[offset + 1] as usize) << 8)
20007 | wire[offset + 2] as usize;
20008 let frame_type = wire[offset + 3];
20009 let stream_id = u32::from_be_bytes([
20010 wire[offset + 5],
20011 wire[offset + 6],
20012 wire[offset + 7],
20013 wire[offset + 8],
20014 ]) & 0x7fff_ffff;
20015 let start = offset + parser::FRAME_HEADER_SIZE;
20016 let end = start + payload_len;
20017 assert!(
20018 end <= wire.len(),
20019 "captured frame must be complete: {payload_len} payload bytes \
20020 declared at offset {offset} of a {} byte capture",
20021 wire.len()
20022 );
20023 if frame_type == 1 {
20024 blocks.push((stream_id, wire[start..end].to_vec()));
20025 }
20026 offset = end;
20027 }
20028 blocks
20029 }
20030
20031 /// `true` when `block` opens with an RFC 7541 §6.3 dynamic-table-size
20032 /// update. The representation prefixes partition the first byte with no
20033 /// overlap — `1xxxxxxx` indexed, `01xxxxxx` literal-with-indexing,
20034 /// `0001xxxx` literal-never-indexed, `0000xxxx` literal-without-indexing
20035 /// — so the `001xxxxx` pattern identifies the update on its own,
20036 /// whatever integer follows it.
20037 fn starts_with_size_update(block: &[u8]) -> bool {
20038 block.first().is_some_and(|b| b & 0xE0 == 0x20)
20039 }
20040
20041 /// Drive exactly ONE `write_streams` pass that emits a response header
20042 /// block on every `stream_ids` entry, and return those blocks in the
20043 /// order they reached the wire together with whatever size-update the
20044 /// connection still has queued afterwards.
20045 ///
20046 /// Each stream is opened by a real HEADERS frame from the peer and then
20047 /// filled with sozu's own 404 default answer through
20048 /// `answers::set_default_answer`, the same chokepoint the routing layer
20049 /// uses — so the blocks captured here are the ones production encodes.
20050 ///
20051 /// `pending_table_size_update` is installed the way `handle_settings_frame`
20052 /// installs it: the encoder cap and the queued signal move together.
20053 fn drive_one_write_pass(
20054 stream_ids: &[StreamId],
20055 pending_table_size_update: Option<u32>,
20056 ) -> (Vec<(StreamId, Vec<u8>)>, Option<u32>) {
20057 use std::io::{Read, Write};
20058
20059 let pool = Rc::new(RefCell::new(Pool::with_capacity(8, 40, 16_384)));
20060 let (mut connection, mut peer) = test_h2_connection(&pool, None);
20061 let mut context = test_context(&pool);
20062 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
20063
20064 connection.core.state = H2State::Header;
20065 connection
20066 .core
20067 .stream_table
20068 .set_expect_read(Some((H2StreamId::Zero, 9)));
20069
20070 let mut peer_encoder = crate::protocol::mux::hpack::Encoder::new();
20071 for &stream_id in stream_ids {
20072 let field_block = peer_encoder.encode([
20073 (&b":method"[..], &b"GET"[..]),
20074 (&b":scheme"[..], &b"https"[..]),
20075 (&b":authority"[..], &b"example.com"[..]),
20076 (&b":path"[..], &b"/write-pass"[..]),
20077 ]);
20078 let mut frame = Vec::with_capacity(parser::FRAME_HEADER_SIZE + field_block.len());
20079 frame.extend_from_slice(&(field_block.len() as u32).to_be_bytes()[1..]);
20080 frame.push(1); // HEADERS
20081 frame.push(parser::FLAG_END_STREAM | parser::FLAG_END_HEADERS);
20082 frame.extend_from_slice(&stream_id.to_be_bytes());
20083 frame.extend_from_slice(&field_block);
20084 peer.write_all(&frame)
20085 .expect("loopback write must complete");
20086 peer.flush().expect("loopback flush must complete");
20087 for _ in 0..64 {
20088 connection.readable(&mut context, EndpointClient(&mut router));
20089 if connection
20090 .core
20091 .stream_table
20092 .streams()
20093 .contains_key(&stream_id)
20094 {
20095 break;
20096 }
20097 std::thread::yield_now();
20098 }
20099 assert!(
20100 connection
20101 .core
20102 .stream_table
20103 .streams()
20104 .contains_key(&stream_id),
20105 "stream {stream_id} must be accepted before the write pass"
20106 );
20107 }
20108
20109 let answers = context.listener.borrow().get_answers().clone();
20110 let open: Vec<GlobalStreamId> = connection
20111 .core
20112 .stream_table
20113 .streams()
20114 .values()
20115 .copied()
20116 .collect();
20117 for global_stream_id in open {
20118 crate::protocol::mux::answers::set_default_answer(
20119 &mut context.streams[global_stream_id],
20120 &mut connection.core.readiness,
20121 404,
20122 &answers.borrow(),
20123 );
20124 }
20125
20126 // Discard whatever the handshake already put on the wire, so the
20127 // capture below holds this write pass and nothing else.
20128 let mut discard = Vec::new();
20129 let _ = peer.read_to_end(&mut discard);
20130
20131 if let Some(size) = pending_table_size_update {
20132 connection
20133 .core
20134 .hpack
20135 .set_encoder_max_table_size(size as usize);
20136 }
20137 connection.core.pending_table_size_update = pending_table_size_update;
20138 connection.writable(&mut context, EndpointClient(&mut router));
20139
20140 let mut wire = Vec::new();
20141 let _ = peer.read_to_end(&mut wire);
20142 (
20143 headers_blocks(&wire),
20144 connection.core.pending_table_size_update,
20145 )
20146 }
20147
20148 /// Two properties of a `write_streams` pass, both invisible to any
20149 /// single-stream test, pinned on the bytes two streams actually receive:
20150 ///
20151 /// 1. the RFC 7541 §6.3 dynamic-table-size-update prefix is written to
20152 /// the FIRST header block of the pass and to no other, and the
20153 /// connection's mirror is cleared only because a block carried it;
20154 /// 2. both blocks are encoded against ONE encoder dynamic table, so a
20155 /// peer replaying them in order with a single decoder stays in sync —
20156 /// and a decoder that never saw the first block CANNOT read the
20157 /// second, which is the negative half that makes assertion 2
20158 /// discriminating rather than vacuous.
20159 ///
20160 /// Scoping the `H2BlockConverter` to a single `kawa.prepare` call is
20161 /// what puts both at risk: the converter no longer spans the per-stream
20162 /// loop, so the signal and the encoder now cross from one stream to the
20163 /// next through [`converter::H2ConverterPass`] rather than through one
20164 /// long-lived struct.
20165 ///
20166 /// TO SEE THIS RED, break either half:
20167 /// (a) delete `self.pending_table_size_update = converter.pending_table_size_update;`
20168 /// from `converter::H2ConverterPass::reclaim` — every block of the
20169 /// pass then re-emits the prefix, and the second assertion fails with
20170 /// `only the FIRST header block of a pass carries the size update;
20171 /// stream 3 opened with 0x3f`;
20172 /// (b) pass a fresh `crate::protocol::mux::hpack::Encoder::new()` to `pass.converter(..)`
20173 /// in `write_streams` instead of `self.hpack.encoder_mut()` — every
20174 /// block becomes self-contained and the LAST assertion fails with
20175 /// `the second block must depend on the dynamic table the first one
20176 /// built`.
20177 #[test]
20178 fn one_write_pass_prefixes_its_first_header_block_only_and_shares_one_encoder() {
20179 // 4096 is the HPACK default table size, so `[0x3f, 0xe1, 0x1f]` below
20180 // is the canonical §6.3 encoding: prefix bits `001`, a 5-bit prefix
20181 // integer saturated to 31, then 4096 - 31 = 4065 as two continuation
20182 // octets (0x61 | 0x80, 0x1f). Written out rather than produced with
20183 // the same encoder the production path uses, so the test cannot
20184 // agree with a broken encoder.
20185 const TABLE_SIZE: u32 = 4096;
20186 const SIZE_UPDATE_PREFIX: [u8; 3] = [0x3f, 0xe1, 0x1f];
20187
20188 let (blocks, residual) = drive_one_write_pass(&[1, 3], Some(TABLE_SIZE));
20189
20190 assert_eq!(
20191 blocks.iter().map(|(id, _)| *id).collect::<Vec<_>>(),
20192 vec![1, 3],
20193 "one pass must emit one header block per open stream, in the \
20194 priority order (equal urgency, ascending stream id)"
20195 );
20196
20197 assert_eq!(
20198 &blocks[0].1[..SIZE_UPDATE_PREFIX.len()],
20199 &SIZE_UPDATE_PREFIX,
20200 "the first header block of the pass must open with the \
20201 dynamic-table-size-update for {TABLE_SIZE}"
20202 );
20203 assert!(
20204 !starts_with_size_update(&blocks[1].1),
20205 "only the FIRST header block of a pass carries the size update; \
20206 stream {} opened with {:#04x}",
20207 blocks[1].0,
20208 blocks[1].1[0]
20209 );
20210 assert_eq!(
20211 residual, None,
20212 "the connection clears its mirror once a block carried the signal"
20213 );
20214
20215 // Positive half: the peer's single decoder replays the pass in order.
20216 let mut peer_decoder = crate::protocol::mux::hpack::Decoder::new();
20217 peer_decoder.set_max_allowed_table_size(TABLE_SIZE as usize);
20218 for (stream_id, block) in &blocks {
20219 let mut decoded = Vec::new();
20220 let status = peer_decoder.decode_with_cb(block, |key, value| {
20221 decoded.push((key.into_owned(), value.into_owned()));
20222 });
20223 assert!(
20224 status.is_ok(),
20225 "a peer replaying the pass in order must decode stream \
20226 {stream_id}'s block: {status:?}"
20227 );
20228 assert!(
20229 decoded.contains(&(b":status".to_vec(), b"404".to_vec())),
20230 "stream {stream_id}'s block must carry the 404 status line, \
20231 got {decoded:?}"
20232 );
20233 }
20234
20235 // Negative half: the second block is not self-contained. It indexes
20236 // dynamic-table entries only the first block created, so a decoder
20237 // that never saw the first block cannot read it. Without this, a
20238 // per-stream encoder would satisfy every assertion above.
20239 let mut fresh_decoder = crate::protocol::mux::hpack::Decoder::new();
20240 fresh_decoder.set_max_allowed_table_size(TABLE_SIZE as usize);
20241 let status = fresh_decoder.decode_with_cb(&blocks[1].1, |_, _| {});
20242 assert!(
20243 status.is_err(),
20244 "the second block must depend on the dynamic table the first one \
20245 built — a decoder that never saw the first block must fail on \
20246 it, otherwise the two blocks came from two encoders"
20247 );
20248 }
20249
20250 // ── RFC 9113 §3.4 client connection preface, read-fragmentation axis ──
20251 //
20252 // `Connection::new_h2_server` arms `expect_read` with
20253 // `CLIENT_PREFACE_SIZE` (the 24-octet magic string plus a 9-octet
20254 // SETTINGS frame header). When a read comes back SHORT of that request,
20255 // the short-read branch of `H2Shell::readable` runs an early guard
20256 // so a client that is plainly not speaking H2 is dropped without waiting
20257 // for the rest of the window.
20258 //
20259 // Nothing about a TCP segment or a TLS record guarantees it lands on the
20260 // 24-octet boundary, so that guard has to tolerate EVERY split of a
20261 // byte-perfect preface while still refusing a wrong one.
20262
20263 /// The whole accumulated window, at every chunk size, is the axis that
20264 /// matters here — not one hand-picked split. The failure this pins is
20265 /// invisible at exactly 24 (the window never grows past the magic
20266 /// string, so a prefix test against it still answers correctly) and at
20267 /// exactly 33 (the request is filled, so the short-read branch is never
20268 /// entered at all) — the two sizes a hand-written test is most likely to
20269 /// pick. So the sweep collects every failing size instead of asserting
20270 /// inside the loop, which would stop at the first one and hide the band.
20271 ///
20272 /// To SEE THIS RED: in the `H2State::ClientPreface` arm of
20273 /// `H2Shell::readable`'s short-read branch, compare the whole
20274 /// accumulated window instead of the part the constant can cover —
20275 /// `if !pri.starts_with(i)` in place of
20276 /// `if !pri.starts_with(&i[..i.len().min(pri.len())])`. `starts_with`
20277 /// asks whether the window is a prefix OF the 24-octet constant, so it
20278 /// answers false for every window longer than 24 whatever the window
20279 /// holds, and this reports `[1, 25, 26, 27, 28, 29, 30, 31, 32]`.
20280 #[test]
20281 fn a_byte_perfect_client_preface_survives_every_read_fragmentation() {
20282 use std::io::Write;
20283
20284 // Built from the wire bytes, not from `serializer::H2_PRI`, so this
20285 // also pins the constant's value rather than agreeing with whatever
20286 // it happens to hold.
20287 let mut wire = Vec::with_capacity(CLIENT_PREFACE_SIZE);
20288 wire.extend_from_slice(b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n");
20289 // SETTINGS, empty payload, no flags, stream 0 — RFC 9113 §6.5.
20290 wire.extend_from_slice(&[0, 0, 0, 4, 0, 0, 0, 0, 0]);
20291 assert_eq!(
20292 wire.len(),
20293 CLIENT_PREFACE_SIZE,
20294 "the probe must be exactly the window the server asks for"
20295 );
20296
20297 let mut disconnected = Vec::new();
20298
20299 for chunk in 1..=40usize {
20300 let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16_384)));
20301 let (mut connection, mut peer) = test_h2_connection(&pool, None);
20302 let mut context = test_context(&pool);
20303 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
20304
20305 let mut delivered = 0usize;
20306 let mut closed = false;
20307
20308 'delivery: for piece in wire.chunks(chunk) {
20309 peer.write_all(piece).expect("loopback write must complete");
20310 peer.flush().expect("loopback flush must complete");
20311 delivered += piece.len();
20312
20313 // Drive until THIS piece has been consumed before writing the
20314 // next one. Two loopback writes the connection has not read
20315 // between coalesce in the kernel receive queue, which would
20316 // hand `socket_read` a full 33-octet window, take the
20317 // not-short path, and never reach the guard under test.
20318 for _ in 0..64 {
20319 if !matches!(connection.core.state, H2State::ClientPreface) {
20320 break 'delivery;
20321 }
20322 if matches!(
20323 connection.readable(&mut context, EndpointClient(&mut router)),
20324 MuxResult::CloseSession
20325 ) {
20326 closed = true;
20327 break 'delivery;
20328 }
20329 if connection.core.stream_table.expect_read()
20330 == Some((H2StreamId::Zero, CLIENT_PREFACE_SIZE - delivered))
20331 {
20332 break;
20333 }
20334 std::thread::yield_now();
20335 }
20336 }
20337
20338 if closed
20339 || matches!(
20340 connection.core.state,
20341 H2State::ClientPreface | H2State::Error | H2State::GoAway
20342 )
20343 {
20344 disconnected.push(chunk);
20345 }
20346 }
20347
20348 assert!(
20349 disconnected.is_empty(),
20350 "a byte-perfect client preface must be accepted however the \
20351 reads are fragmented; these chunk sizes were disconnected \
20352 instead: {disconnected:?}"
20353 );
20354 }
20355
20356 /// The other half of the same guard: clipping the comparison to the part
20357 /// the magic string can cover must not blunt it. A window that is wrong
20358 /// INSIDE the first 24 octets is still refused while the request is
20359 /// unfilled — which is the only moment the early guard can act, since
20360 /// `parser::preface` in the `H2State::ClientPreface` arm is reached only
20361 /// once the whole `CLIENT_PREFACE_SIZE` window has been read.
20362 ///
20363 /// Corrupting the FIRST and the LAST octet of the magic string proves
20364 /// the comparison still spans all 24, and sweeping the window across the
20365 /// 25..=32 band proves the fix did not turn the band into a hole that
20366 /// lets a non-H2 client linger.
20367 ///
20368 /// To SEE THIS RED: drop the guard's `!`, or clip the comparison to
20369 /// fewer octets than the magic string holds — e.g.
20370 /// `&i[..i.len().min(pri.len() - 1)]`, which stops refusing the
20371 /// `corrupt_at = 23` cases.
20372 #[test]
20373 fn an_invalid_client_preface_is_still_refused_before_the_window_is_filled() {
20374 use std::io::Write;
20375
20376 let mut survivors = Vec::new();
20377
20378 for corrupt_at in [0usize, 23] {
20379 for window in (corrupt_at + 1)..CLIENT_PREFACE_SIZE {
20380 let mut wire = Vec::with_capacity(CLIENT_PREFACE_SIZE);
20381 wire.extend_from_slice(b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n");
20382 wire.extend_from_slice(&[0, 0, 0, 4, 0, 0, 0, 0, 0]);
20383 wire[corrupt_at] ^= 0xff;
20384 wire.truncate(window);
20385
20386 let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16_384)));
20387 let (mut connection, mut peer) = test_h2_connection(&pool, None);
20388 let mut context = test_context(&pool);
20389 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
20390
20391 peer.write_all(&wire).expect("loopback write must complete");
20392 peer.flush().expect("loopback flush must complete");
20393
20394 // The window is short of `CLIENT_PREFACE_SIZE`, so the
20395 // connection never leaves `H2State::ClientPreface` on its
20396 // own: whatever refuses it here is the early guard.
20397 for _ in 0..64 {
20398 if matches!(
20399 connection.readable(&mut context, EndpointClient(&mut router)),
20400 MuxResult::CloseSession
20401 ) {
20402 break;
20403 }
20404 std::thread::yield_now();
20405 }
20406
20407 if !matches!(connection.core.state, H2State::Error) {
20408 survivors.push((corrupt_at, window));
20409 }
20410 }
20411 }
20412
20413 assert!(
20414 survivors.is_empty(),
20415 "a preface wrong inside its first 24 octets must be refused \
20416 before the request is filled; these (corrupt_at, window) pairs \
20417 survived: {survivors:?}"
20418 );
20419 }
20420
20421 // ── Property coverage: the HPACK encoder across a multi-stream pass ──
20422 //
20423 // The deterministic test above fixes ONE shape: two streams, the HPACK
20424 // default table size, the size update landing on the first of exactly
20425 // two blocks. This property generalises the stream count (2..=5) and the
20426 // advertised table size, which decides both how many octets the §6.3
20427 // prefix occupies and how much dynamic table the later blocks can index.
20428 //
20429 // It is a sibling of `reassembly_property` below rather than a case
20430 // inside it: that one drives the READ path and its oracle is "one
20431 // decoder stays in sync across a fragmented header block", while this
20432 // one drives the WRITE path and its oracle is "one encoder, one prefix,
20433 // N blocks a single peer decoder replays". One `quickcheck` verdict over
20434 // two unrelated state machines would say nothing about either.
20435 //
20436 // The negative half of the deterministic test — a fresh decoder must
20437 // FAIL on a later block — is deliberately left out here: a generated
20438 // table size small enough to evict the dynamic table makes every block
20439 // self-contained, which is correct behaviour and would fail that
20440 // assertion.
20441 mod write_pass_property {
20442 use quickcheck::{Arbitrary, Gen, TestResult, quickcheck};
20443
20444 use super::*;
20445
20446 /// One generated write pass: how many streams share it, and the
20447 /// `SETTINGS_HEADER_TABLE_SIZE` the peer advertised just before it.
20448 #[derive(Debug, Clone, Copy)]
20449 struct WritePassPlan {
20450 streams: u8,
20451 table_size: u16,
20452 }
20453
20454 impl Arbitrary for WritePassPlan {
20455 fn arbitrary(g: &mut Gen) -> Self {
20456 WritePassPlan {
20457 streams: *g.choose(&[2u8, 3, 4, 5]).expect("non-empty slice"),
20458 // Bounded by the HPACK default a fresh `Decoder`
20459 // accepts, mirrored onto the peer decoder below; a
20460 // larger advertisement is a SETTINGS-path concern, not
20461 // a write-pass one.
20462 table_size: u16::arbitrary(g) % 4097,
20463 }
20464 }
20465 }
20466
20467 fn drive(plan: WritePassPlan) -> TestResult {
20468 let stream_ids: Vec<StreamId> =
20469 (0..u32::from(plan.streams)).map(|i| 1 + 2 * i).collect();
20470 let (blocks, residual) =
20471 drive_one_write_pass(&stream_ids, Some(u32::from(plan.table_size)));
20472
20473 if blocks
20474 .iter()
20475 .map(|(id, _)| *id)
20476 .ne(stream_ids.iter().copied())
20477 {
20478 return TestResult::error(format!(
20479 "plan {plan:?}: expected one block per stream in priority \
20480 order {stream_ids:?}, got {:?}",
20481 blocks.iter().map(|(id, _)| *id).collect::<Vec<_>>()
20482 ));
20483 }
20484 if !starts_with_size_update(&blocks[0].1) {
20485 return TestResult::error(format!(
20486 "plan {plan:?}: the first block must open with the \
20487 size update, got {:#04x}",
20488 blocks[0].1[0]
20489 ));
20490 }
20491 for (stream_id, block) in blocks.iter().skip(1) {
20492 if starts_with_size_update(block) {
20493 return TestResult::error(format!(
20494 "plan {plan:?}: stream {stream_id} re-emitted the \
20495 size update the first block already carried"
20496 ));
20497 }
20498 }
20499 if residual.is_some() {
20500 return TestResult::error(format!(
20501 "plan {plan:?}: the mirror must be cleared once a block \
20502 carried the signal, still {residual:?}"
20503 ));
20504 }
20505
20506 let mut peer_decoder = crate::protocol::mux::hpack::Decoder::new();
20507 peer_decoder.set_max_allowed_table_size(usize::from(plan.table_size));
20508 for (stream_id, block) in &blocks {
20509 let mut decoded = Vec::new();
20510 let status = peer_decoder.decode_with_cb(block, |key, value| {
20511 decoded.push((key.into_owned(), value.into_owned()));
20512 });
20513 if status.is_err() {
20514 return TestResult::error(format!(
20515 "plan {plan:?}: a peer replaying the pass in order \
20516 desynced on stream {stream_id}: {status:?}"
20517 ));
20518 }
20519 if !decoded.contains(&(b":status".to_vec(), b"404".to_vec())) {
20520 return TestResult::error(format!(
20521 "plan {plan:?}: stream {stream_id} decoded to \
20522 {decoded:?}, expected the 404 status line"
20523 ));
20524 }
20525 }
20526 TestResult::passed()
20527 }
20528
20529 quickcheck! {
20530 fn qc_one_write_pass_prefixes_its_first_header_block_only(plan: WritePassPlan) -> TestResult {
20531 drive(plan)
20532 }
20533 }
20534 }
20535
20536 // ── Property coverage: interleaved reassembly + control-frame flushes ──
20537 //
20538 // The four deterministic pinning tests above each fix ONE interleaving
20539 // (an unrelated WINDOW_UPDATE flush, a graceful_goaway, a HUP-while-
20540 // draining event between frames, a HUP-while-draining event mid-TCP-
20541 // segment) at ONE point. This property generalises them: a
20542 // `quickcheck`-driven `ReassemblyPlan` splits a real HPACK field block
20543 // into 2..=5 CONTINUATION fragments and independently chooses, before
20544 // each one, whether to interleave nothing, an unrelated WINDOW_UPDATE
20545 // flush, a `graceful_goaway`, or a HUP-while-draining event — the same
20546 // four triggers, now composed in every order and count `quickcheck`
20547 // cares to generate — and, independently again, whether that interleave
20548 // lands BETWEEN two frames or in the MIDDLE of one frame's own TCP-level
20549 // write (review of `e1c3c2fb`, B2/B3: the two shapes are not
20550 // interchangeable — `zero.storage` is empty in the former and holds a
20551 // genuine partial `socket_read()` in the latter, and only the latter
20552 // reaches the narrower guard the four deterministic tests individually
20553 // pin). `quickcheck` is already a workspace dependency (see
20554 // `lib/src/router/mod.rs`'s `qc_router_hostname_resolution_matches_
20555 // the_documented_semantics`, whose `Arbitrary`/`quickcheck!` shape this
20556 // mirrors).
20557 //
20558 // Any out-of-bounds read or other panic inside `drive()` fails the
20559 // `quickcheck` run directly — there is no separate assertion for it.
20560 // CONTINUATION-flood abort and the CVE-2024-27316 refusal path are
20561 // deliberately NOT folded into this property: they end the block in a
20562 // *different* terminal state (refused, not decoded), which would split
20563 // this property's single pass/fail oracle in two. That path already has
20564 // dedicated deterministic coverage —
20565 // `a_refused_stream_keeps_the_hpack_decoder_in_sync` and
20566 // `a_refused_padded_prioritized_stream_keeps_the_hpack_decoder_in_sync`
20567 // above.
20568 mod reassembly_property {
20569 use quickcheck::{Arbitrary, Gen, TestResult, quickcheck};
20570
20571 use super::*;
20572
20573 /// One interleaved event the property driver may inject before a
20574 /// CONTINUATION fragment. Mirrors the four deterministic pinning
20575 /// tests' triggers.
20576 #[derive(Debug, Clone, Copy)]
20577 enum Interleave {
20578 None,
20579 UnrelatedWindowUpdateFlush,
20580 HupWhileDraining,
20581 GracefulGoaway,
20582 }
20583
20584 impl Arbitrary for Interleave {
20585 fn arbitrary(g: &mut Gen) -> Self {
20586 *g.choose(&[
20587 Interleave::None,
20588 Interleave::UnrelatedWindowUpdateFlush,
20589 Interleave::HupWhileDraining,
20590 Interleave::GracefulGoaway,
20591 ])
20592 .expect("the choice slice above is non-empty")
20593 }
20594 }
20595
20596 /// One interleave point, paired with whether it lands between two
20597 /// frames (`mid_frame == false`, the original shape) or in the
20598 /// middle of the following fragment's own TCP-level write
20599 /// (`mid_frame == true` — a genuine partial `socket_read()`, the
20600 /// shape B2 proved is NOT interchangeable with the former).
20601 #[derive(Debug, Clone, Copy)]
20602 struct InterleavePoint {
20603 interleave: Interleave,
20604 mid_frame: bool,
20605 }
20606
20607 impl Arbitrary for InterleavePoint {
20608 fn arbitrary(g: &mut Gen) -> Self {
20609 InterleavePoint {
20610 interleave: Interleave::arbitrary(g),
20611 mid_frame: bool::arbitrary(g),
20612 }
20613 }
20614 }
20615
20616 /// How many CONTINUATION frames to split the field block into
20617 /// (2..=5) and which [`InterleavePoint`] to inject before each one
20618 /// after the first — exactly `splits - 1` points, one per fragment
20619 /// boundary, never `splits` (there is no point "before the first
20620 /// fragment": the HEADERS frame that carries it is what starts the
20621 /// reassembly).
20622 #[derive(Debug, Clone)]
20623 struct ReassemblyPlan {
20624 splits: u8,
20625 points: Vec<InterleavePoint>,
20626 }
20627
20628 impl Arbitrary for ReassemblyPlan {
20629 fn arbitrary(g: &mut Gen) -> Self {
20630 let splits = 2 + (u8::arbitrary(g) % 4); // 2..=5
20631 let points = (0..splits - 1)
20632 .map(|_| InterleavePoint::arbitrary(g))
20633 .collect();
20634 ReassemblyPlan { splits, points }
20635 }
20636 }
20637
20638 /// Apply one interleave. `GracefulGoaway` checks
20639 /// `connection.drain.draining()` — the AUTHORITATIVE state, not a
20640 /// separately tracked bool — before calling `graceful_goaway`:
20641 /// once the connection is already draining (whether a prior point
20642 /// in this same plan called `graceful_goaway` itself, OR a prior
20643 /// `HupWhileDraining` point set it via the test-only
20644 /// `__test_set_draining()` backdoor), a SECOND `graceful_goaway`
20645 /// call would be the RFC 9113 §6.8 FINAL GOAWAY
20646 /// (`GracefulDrainDecision::AlreadyDraining`), which drops
20647 /// `expect_read` and makes the block permanently uncompletable — a
20648 /// different scenario than this property drives, so a
20649 /// `GracefulGoaway` point reached while already draining is
20650 /// treated as `None` instead. A local `bool` tracking only
20651 /// "did GracefulGoaway itself fire" would miss the
20652 /// `HupWhileDraining` case and was exactly the bug `quickcheck`
20653 /// found while writing this property (review of `e1c3c2fb`, B3).
20654 fn apply_interleave<L>(
20655 connection: &mut H2Shell<mio::net::TcpStream>,
20656 context: &mut Context<L>,
20657 router: &mut Router,
20658 interleave: Interleave,
20659 ) where
20660 L: ListenerHandler + L7ListenerHandler,
20661 {
20662 match interleave {
20663 Interleave::None => {}
20664 Interleave::UnrelatedWindowUpdateFlush => {
20665 connection.core.queue_window_update(0, 65_535);
20666 connection.writable(context, EndpointClient(router));
20667 }
20668 Interleave::HupWhileDraining => {
20669 connection.core.drain.__test_set_draining();
20670 connection.core.readiness.event.insert(Ready::HUP);
20671 connection.writable(context, EndpointClient(router));
20672 connection.core.readiness.event.remove(Ready::HUP);
20673 }
20674 Interleave::GracefulGoaway => {
20675 if !connection.core.drain.draining() {
20676 let now = connection.core.now;
20677 connection.graceful_goaway(now);
20678 }
20679 }
20680 }
20681 }
20682
20683 /// Build the connection, split a real field block into
20684 /// `plan.splits` fragments, and send them as HEADERS + N-1
20685 /// CONTINUATION frames, injecting `plan.points[i - 1]` before
20686 /// fragment `i` (`i >= 1`) — either between the two frames, or
20687 /// split across two TCP-level writes of that fragment's OWN
20688 /// payload with the interleave firing in between. Asserts the
20689 /// connection stays usable and the block decodes byte-for-byte
20690 /// regardless of which interleaves fired or where.
20691 fn drive(plan: ReassemblyPlan) -> TestResult {
20692 use std::io::Write;
20693
20694 let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 20, 16_384)));
20695 let (mut connection, mut peer) = test_h2_connection(&pool, None);
20696 // The interleaved WINDOW_UPDATE flush really writes to the socket
20697 // since #1604 (it used to be deferred while a block was being
20698 // reassembled). Once sozu's end sends data, Linux switches it to
20699 // delayed ACKs ("pingpong" mode), so the peer's last segment is
20700 // acknowledged up to 40 ms late, and the peer's Nagle holds its
20701 // next small CONTINUATION until that ACK: longer than the
20702 // yield-bounded read loops below wait. H2 clients disable Nagle.
20703 peer.set_nodelay(true)
20704 .expect("the loopback peer must accept TCP_NODELAY");
20705 let mut context = test_context(&pool);
20706 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
20707
20708 connection.core.state = H2State::Header;
20709 connection
20710 .core
20711 .stream_table
20712 .set_expect_read(Some((H2StreamId::Zero, 9)));
20713
20714 let mut peer_encoder = crate::protocol::mux::hpack::Encoder::new();
20715 let field_block = peer_encoder.encode([
20716 (&b":method"[..], &b"GET"[..]),
20717 (&b":scheme"[..], &b"https"[..]),
20718 (&b":authority"[..], &b"example.com"[..]),
20719 (&b":path"[..], &b"/qc-multiframe"[..]),
20720 (&b"x-sozu-qc-marker"[..], &b"qc-value"[..]),
20721 ]);
20722 let splits = plan.splits as usize;
20723 if field_block.len() < splits * 2 {
20724 // Too few bytes to split `splits` ways meaningfully.
20725 return TestResult::discard();
20726 }
20727
20728 let chunk = (field_block.len() / splits).max(1);
20729 let mut fragments: Vec<&[u8]> = Vec::with_capacity(splits);
20730 let mut rest = &field_block[..];
20731 for i in 0..splits {
20732 if i + 1 == splits {
20733 fragments.push(rest);
20734 } else {
20735 let (a, b) = rest.split_at(chunk);
20736 fragments.push(a);
20737 rest = b;
20738 }
20739 }
20740 if fragments.iter().any(|f| f.is_empty()) {
20741 return TestResult::discard();
20742 }
20743
20744 let first = fragments[0];
20745 let mut frame = Vec::with_capacity(9 + first.len());
20746 frame.extend_from_slice(&(first.len() as u32).to_be_bytes()[1..]);
20747 frame.push(1); // HEADERS
20748 frame.push(parser::FLAG_END_STREAM);
20749 frame.extend_from_slice(&1u32.to_be_bytes());
20750 frame.extend_from_slice(first);
20751 if peer.write_all(&frame).is_err() || peer.flush().is_err() {
20752 return TestResult::discard();
20753 }
20754
20755 for _ in 0..64 {
20756 connection.readable(&mut context, EndpointClient(&mut router));
20757 if matches!(connection.core.state, H2State::ContinuationHeader(_)) {
20758 break;
20759 }
20760 std::thread::yield_now();
20761 }
20762 if !matches!(connection.core.state, H2State::ContinuationHeader(_)) {
20763 return TestResult::discard();
20764 }
20765
20766 for (idx, fragment) in fragments.iter().enumerate().skip(1) {
20767 let point = plan.points[idx - 1];
20768 let is_last = idx + 1 == fragments.len();
20769 let end_headers_flag = if is_last { parser::FLAG_END_HEADERS } else { 0 };
20770
20771 if point.mid_frame && fragment.len() >= 2 {
20772 // Split THIS fragment's own payload into two TCP-level
20773 // writes, firing the interleave in between — a genuine
20774 // partial `socket_read()`, not a gap between frames.
20775 let mid = fragment.len() / 2;
20776 let (segment_1, segment_2) = fragment.split_at(mid);
20777
20778 let mut header_and_segment_1 = Vec::with_capacity(9 + segment_1.len());
20779 header_and_segment_1
20780 .extend_from_slice(&(fragment.len() as u32).to_be_bytes()[1..]);
20781 header_and_segment_1.push(9); // CONTINUATION
20782 header_and_segment_1.push(end_headers_flag);
20783 header_and_segment_1.extend_from_slice(&1u32.to_be_bytes());
20784 header_and_segment_1.extend_from_slice(segment_1);
20785 if peer.write_all(&header_and_segment_1).is_err() || peer.flush().is_err() {
20786 return TestResult::discard();
20787 }
20788
20789 for _ in 0..64 {
20790 connection.readable(&mut context, EndpointClient(&mut router));
20791 if matches!(connection.core.state, H2State::ContinuationFrame(_))
20792 && matches!(
20793 connection.core.stream_table.expect_read(),
20794 Some((H2StreamId::Zero, remaining)) if remaining == segment_2.len()
20795 )
20796 {
20797 break;
20798 }
20799 std::thread::yield_now();
20800 }
20801 if !matches!(connection.core.state, H2State::ContinuationFrame(_)) {
20802 // The segment split did not land as a genuine
20803 // partial read (e.g. too small to observe) — treat
20804 // as a discard rather than a false failure.
20805 return TestResult::discard();
20806 }
20807
20808 apply_interleave(&mut connection, &mut context, &mut router, point.interleave);
20809
20810 if peer.write_all(segment_2).is_err() || peer.flush().is_err() {
20811 return TestResult::discard();
20812 }
20813 } else {
20814 apply_interleave(&mut connection, &mut context, &mut router, point.interleave);
20815
20816 let mut cframe = Vec::with_capacity(9 + fragment.len());
20817 cframe.extend_from_slice(&(fragment.len() as u32).to_be_bytes()[1..]);
20818 cframe.push(9); // CONTINUATION
20819 cframe.push(end_headers_flag);
20820 cframe.extend_from_slice(&1u32.to_be_bytes());
20821 cframe.extend_from_slice(fragment);
20822 if peer.write_all(&cframe).is_err() || peer.flush().is_err() {
20823 return TestResult::discard();
20824 }
20825 }
20826
20827 for _ in 0..64 {
20828 connection.readable(&mut context, EndpointClient(&mut router));
20829 if !matches!(
20830 connection.core.state,
20831 H2State::ContinuationHeader(_) | H2State::ContinuationFrame(_)
20832 ) {
20833 break;
20834 }
20835 std::thread::yield_now();
20836 }
20837 }
20838
20839 if matches!(connection.core.state, H2State::Error | H2State::GoAway) {
20840 return TestResult::error(format!(
20841 "connection reached {:?} while reassembling under plan {plan:?}",
20842 connection.core.state
20843 ));
20844 }
20845
20846 let next_block = peer_encoder.encode([(&b"x-sozu-qc-marker"[..], &b"qc-value"[..])]);
20847 let mut decoded = Vec::new();
20848 let status = connection
20849 .core
20850 .hpack
20851 .decoder_mut()
20852 .decode_with_cb(&next_block, |k, v| {
20853 decoded.push((k.into_owned(), v.into_owned()));
20854 });
20855 if status.is_err() {
20856 return TestResult::error(format!(
20857 "decoder desynced after plan {plan:?}: {status:?}"
20858 ));
20859 }
20860 if decoded != vec![(b"x-sozu-qc-marker".to_vec(), b"qc-value".to_vec())] {
20861 return TestResult::error(format!(
20862 "decoded {decoded:?} after plan {plan:?}, expected the qc marker header"
20863 ));
20864 }
20865
20866 TestResult::passed()
20867 }
20868
20869 quickcheck! {
20870 fn qc_h2_header_reassembly_survives_interleaved_control_frame_flushes(plan: ReassemblyPlan) -> TestResult {
20871 drive(plan)
20872 }
20873 }
20874 }
20875
20876 // ── #1454: the write path over a REAL `FrontRustls` ─────────────────
20877 //
20878 // Every `socket_wants_write()` branch exercised above this point is
20879 // exercised over `BackpressuredTlsSocket` — a TCP handler with a modelled
20880 // answer. #1454 rules that shape out for closing it: `FrontRustls` is the
20881 // only production handler that can answer `true`, and a fixture that fakes
20882 // the answer on a TCP handler tests the fake. These are the first tests in
20883 // the tree that build a `H2Shell<FrontRustls>`;
20884 // `git grep -cE 'H2Shell<FrontRustls>' -- '*.rs'` answered 0 on every
20885 // commit before this one, including on the `write_streams` inversion's head.
20886 //
20887 // What only the real handler can settle: `(size > 0, WouldBlock)` is not a
20888 // scripted pair here. It is what `FrontRustls::socket_write_vectored`
20889 // returns when rustls absorbs plaintext into its own record buffer while
20890 // `write_tls` blocks against a full kernel send queue — `buffered_size` and
20891 // `can_write` are two independent quantities sharing one return value.
20892 // `update_readiness` classifies a pass as stalled iff `size == 0`, so that
20893 // pair is NOT a stall and the flush loop must go round again. Everything
20894 // written against a scripted pair assumes the production handler produces
20895 // it; nothing proved it until these tests.
20896 //
20897 // The handshake runs in memory between a real `rustls::ServerConnection`
20898 // and a real `rustls::ClientConnection`, and only then is the settled
20899 // server session attached to a loopback socket whose kernel queues are
20900 // pinned small. That order matters: a flight never has to fit through the
20901 // shrunken queue, so the backpressure under test is entirely the test's
20902 // and never the handshake's.
20903
20904 use crate::socket::FrontRustls;
20905
20906 /// Accepts whatever certificate the frontend serves.
20907 ///
20908 /// `lib/assets/certificate.pem` is self-signed and carries NO
20909 /// subjectAltName, so rustls's own web-PKI verifier rejects it whatever
20910 /// root store it is given — a CN-only certificate is not verifiable under
20911 /// RFC 6125 §6.4.4 as rustls implements it. The client here is a
20912 /// decryption oracle for bytes the frontend wrote, not a party whose trust
20913 /// decision is under test, so it asserts validity outright. Same shape as
20914 /// `e2e`'s `Verifier`, which exists for the same certificate.
20915 #[derive(Debug)]
20916 struct AcceptAnyServerCertificate;
20917
20918 impl rustls::client::danger::ServerCertVerifier for AcceptAnyServerCertificate {
20919 fn verify_server_cert(
20920 &self,
20921 _end_entity: &rustls::pki_types::CertificateDer<'_>,
20922 _intermediates: &[rustls::pki_types::CertificateDer<'_>],
20923 _server_name: &rustls::pki_types::ServerName<'_>,
20924 _ocsp_response: &[u8],
20925 _now: rustls::pki_types::UnixTime,
20926 ) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
20927 Ok(rustls::client::danger::ServerCertVerified::assertion())
20928 }
20929
20930 fn verify_tls12_signature(
20931 &self,
20932 _message: &[u8],
20933 _cert: &rustls::pki_types::CertificateDer<'_>,
20934 _dss: &rustls::DigitallySignedStruct,
20935 ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
20936 Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
20937 }
20938
20939 fn verify_tls13_signature(
20940 &self,
20941 _message: &[u8],
20942 _cert: &rustls::pki_types::CertificateDer<'_>,
20943 _dss: &rustls::DigitallySignedStruct,
20944 ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
20945 Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
20946 }
20947
20948 fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
20949 vec![
20950 rustls::SignatureScheme::RSA_PKCS1_SHA256,
20951 rustls::SignatureScheme::RSA_PKCS1_SHA384,
20952 rustls::SignatureScheme::RSA_PKCS1_SHA512,
20953 rustls::SignatureScheme::ECDSA_NISTP256_SHA256,
20954 rustls::SignatureScheme::ECDSA_NISTP384_SHA384,
20955 rustls::SignatureScheme::ECDSA_NISTP521_SHA512,
20956 rustls::SignatureScheme::ED25519,
20957 rustls::SignatureScheme::RSA_PSS_SHA256,
20958 rustls::SignatureScheme::RSA_PSS_SHA384,
20959 rustls::SignatureScheme::RSA_PSS_SHA512,
20960 ]
20961 }
20962 }
20963
20964 /// SNI the test certificate is registered under.
20965 const TEST_SNI: &str = "lolcatho.st";
20966
20967 /// Kernel send/receive queue size for the loopback pair, in bytes. Linux
20968 /// doubles and floors the request, so the effective queue is a few KiB —
20969 /// an order of magnitude under rustls's own 64 KiB record buffer, which is
20970 /// what makes `socket_write_vectored` return a partial count AND
20971 /// `WouldBlock` together instead of one or the other.
20972 const PINNED_SOCKET_QUEUE: usize = 4096;
20973
20974 /// Bytes the tests below queue as a response. Larger than rustls's record
20975 /// buffer, so one `socket_write_vectored` cannot absorb it all.
20976 const QUEUED_RESPONSE_LEN: usize = 256 * 1024;
20977
20978 /// Blocks the response is split into, so a gather offers several slices.
20979 const QUEUED_RESPONSE_CHUNK_LEN: usize = QUEUED_RESPONSE_LEN / 8;
20980
20981 /// Hard bound on the drive loops below. Reaching it is a test failure with
20982 /// the byte shortfall attached, never a silent pass.
20983 const MAX_DRIVE_TICKS: usize = 4096;
20984
20985 /// The response bytes, leaked once so blocks can be `Store::Static`.
20986 ///
20987 /// A repeating non-trivial pattern rather than a constant fill: a
20988 /// comparison against a constant fill cannot see bytes delivered out of
20989 /// order or a block delivered twice, which is half of what "no truncation"
20990 /// has to mean.
20991 ///
20992 /// Laid out as whole DATA frames on stream 1, each 16 KiB on the wire: a
20993 /// stream's `kawa.out` only ever holds whole H2 frames, and since #1604
20994 /// the write pass reads their headers to find where the frame a partial
20995 /// write cut ends (`h2_transmit::frame_tail`). The pattern runs through
20996 /// the headers as well, so the oracle still compares every byte.
20997 fn queued_response_body() -> &'static [u8] {
20998 const FRAME_LEN: usize = 16 * 1024;
20999 const PAYLOAD_LEN: usize = FRAME_LEN - 9;
21000 const _: () = assert!(QUEUED_RESPONSE_LEN.is_multiple_of(FRAME_LEN));
21001 static BODY: std::sync::OnceLock<&'static [u8]> = std::sync::OnceLock::new();
21002 BODY.get_or_init(|| {
21003 let mut body: Vec<u8> = (0..QUEUED_RESPONSE_LEN)
21004 .map(|index| (index % 251) as u8)
21005 .collect();
21006 for frame in body.chunks_mut(FRAME_LEN) {
21007 frame[..9].copy_from_slice(&[
21008 (PAYLOAD_LEN >> 16) as u8,
21009 (PAYLOAD_LEN >> 8) as u8,
21010 PAYLOAD_LEN as u8,
21011 0,
21012 0,
21013 0,
21014 0,
21015 0,
21016 1,
21017 ]);
21018 }
21019 Box::leak(body.into_boxed_slice())
21020 })
21021 }
21022
21023 /// Move one TLS flight from `from` to `to`, in memory.
21024 fn pump_tls<A, B>(
21025 from: &mut rustls::ConnectionCommon<A>,
21026 to: &mut rustls::ConnectionCommon<B>,
21027 ) {
21028 let mut wire = Vec::new();
21029 while from.wants_write() {
21030 from.write_tls(&mut wire)
21031 .expect("a TLS flight must serialize into memory");
21032 }
21033 if wire.is_empty() {
21034 return;
21035 }
21036 let mut cursor = std::io::Cursor::new(wire.as_slice());
21037 while (cursor.position() as usize) < wire.len() {
21038 to.read_tls(&mut cursor)
21039 .expect("a TLS flight must be readable from memory");
21040 to.process_new_packets()
21041 .expect("a TLS flight must process cleanly");
21042 }
21043 }
21044
21045 /// A settled TLS 1.3 `FrontRustls` over a loopback socket whose kernel
21046 /// queues are pinned small, the peer end of that socket, and the client
21047 /// session that decrypts what the frontend writes.
21048 ///
21049 /// The peer and the client session are returned rather than kept here
21050 /// because dropping either would close the connection under the handler
21051 /// being tested, and because together they are the only oracle for "the
21052 /// bytes actually arrived".
21053 fn handshaken_front_rustls() -> (FrontRustls, std::net::TcpStream, rustls::ClientConnection) {
21054 let provider = std::sync::Arc::new(crate::crypto::default_provider());
21055
21056 let resolver = std::sync::Arc::new(crate::tls::MutexCertificateResolver::default());
21057 resolver
21058 .0
21059 .lock()
21060 .expect("the test resolver lock must be available")
21061 .add_certificate(&sozu_command::proto::command::AddCertificate {
21062 address: sozu_command::proto::command::SocketAddress::new_v4(127, 0, 0, 1, 8443),
21063 certificate: sozu_command::proto::command::CertificateAndKey {
21064 certificate: include_str!("../../../assets/certificate.pem").to_owned(),
21065 key: include_str!("../../../assets/key.pem").to_owned(),
21066 names: vec![TEST_SNI.to_owned()],
21067 ..Default::default()
21068 },
21069 expired_at: None,
21070 })
21071 .expect("the test certificate must load into the resolver");
21072
21073 let mut server_config = rustls::ServerConfig::builder_with_provider(provider.clone())
21074 .with_protocol_versions(&[&rustls::version::TLS13])
21075 .expect("the test provider must support TLS 1.3")
21076 .with_no_client_auth()
21077 .with_cert_resolver(resolver);
21078 // TLS 1.3 queues NewSessionTicket straight after the client Finished.
21079 // A server that sends tickets answers `socket_wants_write()` with
21080 // `true` before this fixture has queued a single response byte, and
21081 // every test below would then read `true` for the wrong reason.
21082 server_config.send_tls13_tickets = 0;
21083
21084 let client_config = rustls::ClientConfig::builder_with_provider(provider)
21085 .with_protocol_versions(&[&rustls::version::TLS13])
21086 .expect("the test provider must support TLS 1.3")
21087 .dangerous()
21088 .with_custom_certificate_verifier(std::sync::Arc::new(AcceptAnyServerCertificate))
21089 .with_no_client_auth();
21090
21091 let mut server = rustls::ServerConnection::new(std::sync::Arc::new(server_config))
21092 .expect("the test server session must initialize");
21093 let mut client = rustls::ClientConnection::new(
21094 std::sync::Arc::new(client_config),
21095 rustls::pki_types::ServerName::try_from(TEST_SNI)
21096 .expect("the test SNI must be a valid DNS name"),
21097 )
21098 .expect("the test client session must initialize");
21099
21100 for _ in 0..MAX_DRIVE_TICKS {
21101 if !client.is_handshaking()
21102 && !server.is_handshaking()
21103 && !client.wants_write()
21104 && !server.wants_write()
21105 {
21106 break;
21107 }
21108 pump_tls(&mut client, &mut server);
21109 pump_tls(&mut server, &mut client);
21110 }
21111
21112 assert!(
21113 !server.is_handshaking() && !client.is_handshaking(),
21114 "premise: the in-memory handshake must complete, or the session \
21115 under test buffers plaintext instead of encrypting it"
21116 );
21117 assert!(
21118 !server.wants_write(),
21119 "premise: a settled handshake must leave the server holding no \
21120 records, or socket_wants_write() answers true before this fixture \
21121 has queued one response byte"
21122 );
21123
21124 let listener =
21125 std::net::TcpListener::bind("127.0.0.1:0").expect("the test listener must bind");
21126 // Set on the listener so the accepted socket inherits it and the
21127 // advertised window starts small.
21128 socket2::SockRef::from(&listener)
21129 .set_recv_buffer_size(PINNED_SOCKET_QUEUE)
21130 .expect("the test listener receive queue must shrink");
21131 let address = listener
21132 .local_addr()
21133 .expect("the test listener must expose its address");
21134 let front = std::net::TcpStream::connect(address).expect("the loopback connect must land");
21135 let (peer, _peer_address) = listener.accept().expect("the test peer must be accepted");
21136 socket2::SockRef::from(&front)
21137 .set_send_buffer_size(PINNED_SOCKET_QUEUE)
21138 .expect("the frontend send queue must shrink");
21139 socket2::SockRef::from(&peer)
21140 .set_recv_buffer_size(PINNED_SOCKET_QUEUE)
21141 .expect("the peer receive queue must shrink");
21142 front
21143 .set_nonblocking(true)
21144 .expect("mio requires a non-blocking stream");
21145 peer.set_nonblocking(true)
21146 .expect("the peer must not block the drive loop");
21147
21148 let socket = FrontRustls {
21149 stream: mio::net::TcpStream::from_std(front),
21150 session: server,
21151 peer_disconnected: false,
21152 peer_reset: false,
21153 tls_fatal: false,
21154 session_ulid: Ulid::generate(),
21155 configured_peer: None,
21156 recv_memory: Default::default(),
21157 };
21158 (socket, peer, client)
21159 }
21160
21161 /// A server `ConnectionH2` over that handler.
21162 ///
21163 /// `state` is a parameter for the same reason
21164 /// `connection_with_backpressure` takes one: the close decision reads it
21165 /// as `H2State::GoAway` and the write pass reads it as `H2State::Header`.
21166 pub(in crate::protocol::mux) fn rustls_h2_connection(
21167 pool: &Rc<RefCell<Pool>>,
21168 state: H2State,
21169 ) -> (
21170 H2Shell<FrontRustls>,
21171 std::net::TcpStream,
21172 rustls::ClientConnection,
21173 ) {
21174 let (socket, peer, client) = handshaken_front_rustls();
21175 let mut connection = H2Shell::new(
21176 Ulid::generate(),
21177 socket,
21178 Position::Server,
21179 &mut PoolBufferSource::new(Rc::downgrade(pool)),
21180 H2FloodConfig::default(),
21181 H2ConnectionConfig::default(),
21182 Duration::from_secs(30),
21183 None,
21184 Duration::from_secs(30),
21185 Some((H2StreamId::Zero, CLIENT_PREFACE_SIZE)),
21186 Ready::WRITABLE | Ready::HUP | Ready::ERROR,
21187 )
21188 .expect("a pool with free buffers must yield an H2 connection");
21189 connection.core.state = state;
21190 (connection, peer, client)
21191 }
21192
21193 /// Read every TLS record the peer's kernel currently holds, decrypt it,
21194 /// and append the plaintext to `received`.
21195 fn drain_peer(
21196 client: &mut rustls::ClientConnection,
21197 peer: &mut std::net::TcpStream,
21198 received: &mut Vec<u8>,
21199 ) {
21200 loop {
21201 match client.read_tls(peer) {
21202 Ok(0) => break,
21203 Ok(_) => {}
21204 Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => break,
21205 Err(error) => {
21206 panic!("the peer must read the frontend's TLS records: {error:?}")
21207 }
21208 }
21209 client
21210 .process_new_packets()
21211 .expect("the peer must decrypt what the frontend wrote");
21212 take_plaintext(client, received);
21213 }
21214 take_plaintext(client, received);
21215 }
21216
21217 /// Drain the client session's decrypted plaintext buffer.
21218 ///
21219 /// `WouldBlock` here means "nothing decrypted yet", not an error, and
21220 /// `read_to_end` appends whatever it did read before reporting it.
21221 fn take_plaintext(client: &mut rustls::ClientConnection, received: &mut Vec<u8>) {
21222 use std::io::Read as _;
21223 let mut plaintext = Vec::new();
21224 match client.reader().read_to_end(&mut plaintext) {
21225 Ok(_) => {}
21226 Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {}
21227 Err(error) => panic!("the peer must decrypt the frontend's records: {error:?}"),
21228 }
21229 received.extend_from_slice(&plaintext);
21230 }
21231
21232 /// Keep flushing the handler's buffered records while the peer reads,
21233 /// until rustls holds nothing. Returns whether it got there.
21234 fn flush_until_drained(
21235 socket: &mut FrontRustls,
21236 client: &mut rustls::ClientConnection,
21237 peer: &mut std::net::TcpStream,
21238 received: &mut Vec<u8>,
21239 ) -> bool {
21240 for _ in 0..MAX_DRIVE_TICKS {
21241 if !socket.socket_wants_write() {
21242 return true;
21243 }
21244 // Loopback delivery is softirq-driven, not synchronous with
21245 // `write_tls`: give it a slot rather than spinning the CPU.
21246 std::thread::yield_now();
21247 drain_peer(client, peer, received);
21248 socket.socket_write(&[]);
21249 }
21250 !socket.socket_wants_write()
21251 }
21252
21253 /// A TLS frontend whose final GOAWAY went out without `close_notify`
21254 /// behind it: the close its next writable pass decides queues the alert
21255 /// first and waits a pass for it, then starts the lingering close. The
21256 /// first pass's close moves the state to `Error`; the pending linger is
21257 /// what keeps the second pass from closing outright. The client reads
21258 /// the alert, then the EOF of the half-close.
21259 ///
21260 /// TO SEE THIS RED: in `H2Shell::linger_instead_of_closing`, drop the
21261 /// `self.core.linger = Linger::Pending { deadline };` before the deferral.
21262 /// The second pass then returns `CloseSession`.
21263 #[test]
21264 fn a_linger_deferred_behind_close_notify_starts_on_the_next_pass() {
21265 use std::io::Read as _;
21266
21267 let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
21268 let (mut connection, mut peer, mut client) = rustls_h2_connection(&pool, H2State::GoAway);
21269 connection.core.drain.__test_set_draining();
21270 connection.core.graceful_goaway = true;
21271 let mut context = test_context(&pool);
21272 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
21273
21274 let first = connection.writable(&mut context, EndpointClient(&mut router));
21275 assert!(
21276 matches!(first, MuxResult::Continue) && !connection.core.is_lingering(),
21277 "the first pass waits for close_notify, got {first:?}"
21278 );
21279 let mut second = MuxResult::Continue;
21280 for _ in 0..MAX_DRIVE_TICKS {
21281 connection.core.readiness.event.insert(Ready::WRITABLE);
21282 second = connection.writable(&mut context, EndpointClient(&mut router));
21283 if connection.core.is_lingering() || !matches!(second, MuxResult::Continue) {
21284 break;
21285 }
21286 std::thread::yield_now();
21287 }
21288 assert!(
21289 matches!(second, MuxResult::Continue) && connection.core.is_lingering(),
21290 "once close_notify is out the connection lingers, got {second:?}"
21291 );
21292
21293 let mut received = Vec::new();
21294 let mut closed = false;
21295 for _ in 0..MAX_DRIVE_TICKS {
21296 drain_peer(&mut client, &mut peer, &mut received);
21297 let mut plaintext = [0u8; 16];
21298 match client.reader().read(&mut plaintext) {
21299 Ok(0) => {
21300 closed = true;
21301 break;
21302 }
21303 Ok(_) => {}
21304 Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
21305 std::thread::yield_now()
21306 }
21307 Err(error) => panic!("the client must see a clean close: {error:?}"),
21308 }
21309 }
21310 assert!(
21311 closed,
21312 "the client reads close_notify, then the end of the stream"
21313 );
21314 }
21315
21316 /// The final GOAWAY and the TLS `close_notify` leave in ONE write, and the
21317 /// peer reads the GOAWAY before the close.
21318 ///
21319 /// Drives what a client GOAWAY on an idle connection sets off:
21320 /// `goaway(NoError)`, the `writable` pass that flushes it and starts the
21321 /// lingering close, and `close`. Every TLS write in between is counted through
21322 /// `crate::socket::tls_writes`, which counts each `write_tls` holding at
21323 /// least one record, i.e. each `writev(2)` `FrontRustls` issues. The wire
21324 /// is then read raw, so the ORDER is checked on the records themselves
21325 /// (TLS 1.3 hides the inner type, not the length: 5 + 17 + 1 + 16 = 39 for
21326 /// the GOAWAY, 5 + 2 + 1 + 16 = 24 for the alert), and decrypted, so the
21327 /// client sees the 17-byte GOAWAY and then a clean end of stream, which
21328 /// rustls reports only once it has read `close_notify`.
21329 ///
21330 /// TO SEE THIS RED: in `H2Shell::flush_output_to_socket`, drop the
21331 /// `output_flush_closes_connection` branch so every output flush goes
21332 /// through `socket_write`. The GOAWAY then leaves alone and `close` sends the
21333 /// alert in a second write: this test fails on `the final GOAWAY and
21334 /// close_notify must share one TLS write`, with 2 writes. For the order:
21335 /// in `FrontRustls::socket_write_then_close`, call `send_close_notify`
21336 /// before `writer().write`; it fails on the records, `[24, 39]`.
21337 #[test]
21338 fn the_final_goaway_and_close_notify_leave_in_one_tls_write() {
21339 use std::io::Read as _;
21340
21341 let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
21342 let (mut connection, mut peer, mut client) = rustls_h2_connection(&pool, H2State::Header);
21343 let mut context = test_context(&pool);
21344 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
21345
21346 let writes_before = crate::socket::tls_writes();
21347 let queued = connection.core.goaway(H2Error::NoError);
21348 assert!(
21349 matches!(queued, MuxResult::Continue),
21350 "premise: the GOAWAY must serialise, got {queued:?}"
21351 );
21352 let result = connection.writable(&mut context, EndpointClient(&mut router));
21353 // The close that pass decides is a lingering one: the write side is
21354 // shut down behind the GOAWAY and `close_notify`, and what the client
21355 // still sends is drained (`H2Shell::linger_instead_of_closing`).
21356 assert!(
21357 matches!(result, MuxResult::Continue) && connection.core.is_lingering(),
21358 "the pass that flushed the final GOAWAY must start the lingering close, got {result:?}"
21359 );
21360 connection.close(&mut context, EndpointClient(&mut router));
21361 assert_eq!(
21362 crate::socket::tls_writes() - writes_before,
21363 1,
21364 "the final GOAWAY and close_notify must share one TLS write"
21365 );
21366
21367 let mut wire = Vec::new();
21368 for _ in 0..MAX_DRIVE_TICKS {
21369 let mut chunk = [0u8; 256];
21370 match peer.read(&mut chunk) {
21371 Ok(0) => break,
21372 Ok(size) => wire.extend_from_slice(&chunk[..size]),
21373 Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
21374 if wire.len() >= 39 + 24 {
21375 break;
21376 }
21377 std::thread::yield_now();
21378 }
21379 Err(error) => panic!("the peer must read the frontend's records: {error:?}"),
21380 }
21381 }
21382 let mut records = Vec::new();
21383 let mut offset = 0;
21384 while offset + 5 <= wire.len() {
21385 let length = 5 + usize::from(u16::from_be_bytes([wire[offset + 3], wire[offset + 4]]));
21386 records.push(length);
21387 offset += length;
21388 }
21389 assert_eq!(
21390 records,
21391 vec![39, 24],
21392 "the peer must receive the GOAWAY record, then the close_notify record"
21393 );
21394
21395 let mut cursor = std::io::Cursor::new(wire.as_slice());
21396 while (cursor.position() as usize) < wire.len() {
21397 client
21398 .read_tls(&mut cursor)
21399 .expect("the peer must read the frontend's records");
21400 client
21401 .process_new_packets()
21402 .expect("the peer must decrypt the frontend's records");
21403 }
21404 let mut frame = [0u8; 64];
21405 let size = client
21406 .reader()
21407 .read(&mut frame)
21408 .expect("the GOAWAY must be readable before the close");
21409 assert_eq!(size, 17, "the GOAWAY frame is 9 + 8 bytes");
21410 assert_eq!(frame[3], 0x07, "the first plaintext must be a GOAWAY frame");
21411 assert_eq!(
21412 &frame[13..17],
21413 &[0, 0, 0, 0],
21414 "the GOAWAY must carry NO_ERROR"
21415 );
21416 assert_eq!(
21417 client
21418 .reader()
21419 .read(&mut frame)
21420 .expect("a close_notify ends the stream cleanly"),
21421 0,
21422 "the close_notify must follow the GOAWAY"
21423 );
21424 }
21425
21426 /// `FrontRustls` answers a blocked write with bytes taken AND `WouldBlock`,
21427 /// and `update_readiness` calls that pair not-stalled.
21428 ///
21429 /// This is the premise every scripted `(size > 0, WouldBlock)` test in this
21430 /// file rests on, taken here from the production handler instead of from a
21431 /// fixture: rustls absorbs plaintext into its record buffer while
21432 /// `write_tls` blocks against the kernel, so `buffered_size` and
21433 /// `can_write` disagree and one return value carries both answers.
21434 ///
21435 /// TO SEE THIS RED: in `FrontRustls::socket_write_vectored`, report
21436 /// nothing absorbed on a blocked write — change the `!can_write` arm of
21437 /// the final result from `(buffered_size, SocketResult::WouldBlock)` to
21438 /// `(0, SocketResult::WouldBlock)`. That is the under-reporting half of
21439 /// the same class the over-reporting one below belongs to, and the caller
21440 /// then re-offers bytes rustls already holds. This test fails on `rustls
21441 /// takes plaintext off the caller's hands even while the kernel is full,
21442 /// so the handler must report the bytes it absorbed`. Measured: `1092
21443 /// passed; 6 failed` — every test in this block and nothing else, so the
21444 /// whole class was invisible to the suite.
21445 ///
21446 /// Not a red recipe, and measured as such: deleting `can_write = false;`
21447 /// from the `ErrorKind::WouldBlock` arm of the partial-absorb draining
21448 /// loop leaves the entire suite green. The post-loop flush block sets the
21449 /// same flag for the same reason a moment later, so the `WouldBlock` half
21450 /// of this pair is guarded twice in production and no single-site deletion
21451 /// of it is observable here.
21452 #[test]
21453 fn a_real_rustls_frontend_reports_bytes_taken_and_would_block_together() {
21454 let (mut socket, _peer, _client) = handshaken_front_rustls();
21455 let body = queued_response_body();
21456
21457 assert!(
21458 !socket.socket_wants_write(),
21459 "premise: the fixture must start with rustls holding nothing"
21460 );
21461
21462 let (size, status) = socket.socket_write_vectored(&[IoSlice::new(body)]);
21463
21464 assert!(
21465 size > 0,
21466 "rustls takes plaintext off the caller's hands even while the \
21467 kernel is full, so the handler must report the bytes it absorbed"
21468 );
21469 assert!(
21470 size < body.len(),
21471 "premise: the offer must exceed what one call can absorb, or the \
21472 partial-write path that produces this pair is never entered \
21473 (absorbed {size} of {})",
21474 body.len()
21475 );
21476 assert_eq!(
21477 status,
21478 SocketResult::WouldBlock,
21479 "a frontend whose kernel send queue is full must report WouldBlock"
21480 );
21481 assert!(
21482 socket.socket_wants_write(),
21483 "the records rustls could not push must still be waiting, which is \
21484 the only reason any close path has to ask"
21485 );
21486
21487 let mut readiness = Readiness {
21488 event: Ready::WRITABLE,
21489 interest: Ready::WRITABLE | Ready::HUP | Ready::ERROR,
21490 };
21491 assert!(
21492 !update_readiness_after_write(size, status, &mut readiness),
21493 "a write that moved bytes is not a stall however it reported its \
21494 status: a pass that ends here leaves the rest of the response \
21495 queued behind a WouldBlock the rule does not treat as one"
21496 );
21497 }
21498
21499 /// The empty flush the close paths issue really does hand rustls's records
21500 /// to the kernel — `true` before it and `false` after, once the peer reads.
21501 ///
21502 /// `socket_write(&[])` is the middle call of every flush triple in
21503 /// `h2.rs`: those sites discard its `(size, status)` entirely and learn
21504 /// whether it landed only by asking `socket_wants_write()` again. That
21505 /// reconstruction is only sound if a zero-length write flushes at all, and
21506 /// `FrontRustls::socket_write`'s main loop exits immediately for an empty
21507 /// buffer — the flush lives in a block after it, reached by no other path.
21508 ///
21509 /// TO SEE THIS RED: in `FrontRustls::socket_write`, delete the post-loop
21510 /// `if !is_error && !is_closed && can_write && self.session.wants_write()`
21511 /// block. `socket_write(&[])` becomes a no-op, the records never reach the
21512 /// kernel, and this test fails on `once the peer reads, the empty flush
21513 /// must hand every record to the kernel: rustls still holds records after
21514 /// 4096 flushes against a draining peer`. Measured: `1093 passed; 5
21515 /// failed` — five of the six here and nothing that predates them, so the
21516 /// block that comment calls load-bearing had no unit coverage at all.
21517 #[test]
21518 fn an_empty_flush_drains_the_records_a_blocked_write_left_behind() {
21519 let (mut socket, mut peer, mut client) = handshaken_front_rustls();
21520 let body = queued_response_body();
21521
21522 let (absorbed, _status) = socket.socket_write_vectored(&[IoSlice::new(body)]);
21523 assert!(
21524 socket.socket_wants_write(),
21525 "premise: a blocked kernel must leave records pending, or neither \
21526 arm below is reachable"
21527 );
21528
21529 // The kernel is still full: the flush cannot land and the query
21530 // answers `true` on both sides of it. This is the "true twice" arm.
21531 socket.socket_write(&[]);
21532 assert!(
21533 socket.socket_wants_write(),
21534 "a flush against a full kernel leaves the records where they were, \
21535 so the triple's second query must still answer true"
21536 );
21537
21538 // Now the peer reads, and the same flush must land. This is the
21539 // "true then false" arm.
21540 let mut received = Vec::new();
21541 assert!(
21542 flush_until_drained(&mut socket, &mut client, &mut peer, &mut received),
21543 "once the peer reads, the empty flush must hand every record to \
21544 the kernel: rustls still holds records after {MAX_DRIVE_TICKS} \
21545 flushes against a draining peer"
21546 );
21547
21548 for _ in 0..MAX_DRIVE_TICKS {
21549 if received.len() >= absorbed {
21550 break;
21551 }
21552 std::thread::yield_now();
21553 drain_peer(&mut client, &mut peer, &mut received);
21554 }
21555 assert_eq!(
21556 received.len(),
21557 absorbed,
21558 "every plaintext byte rustls reported absorbing must reach the peer"
21559 );
21560 assert_eq!(
21561 received.as_slice(),
21562 &body[..absorbed],
21563 "the peer must receive those bytes unchanged and in order"
21564 );
21565 }
21566
21567 /// A response larger than one pass can deliver reaches the peer in full,
21568 /// driven through `writable()` over the production TLS handler.
21569 ///
21570 /// This is #1454's truncation vector end to end, with no scripted answer
21571 /// anywhere: the `(size > 0, WouldBlock)` and `(0, WouldBlock)` pairs come
21572 /// from `FrontRustls` against a real full kernel, the stall and its resume
21573 /// come from `write_streams`, and the oracle is the bytes a real
21574 /// `rustls::ClientConnection` decrypts on the other end of the socket. The
21575 /// assertion is byte equality, not a call count, so it says nothing about
21576 /// how the write machine is shaped and everything about what it delivers.
21577 ///
21578 /// TO SEE THIS RED: in `FrontRustls::socket_write_vectored`, report the
21579 /// full offer instead of what rustls took — replace the final
21580 /// `(buffered_size, SocketResult::WouldBlock)` with
21581 /// `(total_len, SocketResult::WouldBlock)`. That is the over-reporting
21582 /// shape `socket_write`/`socket_write_vectored`'s own comments call the
21583 /// 4.5 MB truncation-class bug: `h2_transmit::confirm` then consumes bytes
21584 /// the socket never encrypted and they are gone. Measured: `1095 passed; 3
21585 /// failed`, this one on `the response was truncated at 65536 of 262144
21586 /// bytes after 4096 passes, with 0 blocks still queued`. The pass empties
21587 /// `kawa.out` against bytes that were never sent, `finalize_write` reads
21588 /// that as everything written and quiesces, and the remaining 196608 bytes
21589 /// have no route to the wire at all — the connection is not short, it is
21590 /// hung, and the tick cap is what converts that into a failure instead of
21591 /// a wait. Nothing that predates this block moves.
21592 #[test]
21593 fn a_blocked_rustls_frontend_delivers_every_queued_byte_across_passes() {
21594 let pool = make_pool_for_invariant_16();
21595 let (mut connection, mut peer, mut client) = rustls_h2_connection(&pool, H2State::Header);
21596 let mut context = test_context(&pool);
21597 let body = queued_response_body();
21598
21599 let gid = context
21600 .create_stream(Ulid::generate(), 1 << 16)
21601 .expect("test context must create a stream");
21602 connection
21603 .core
21604 .stream_table
21605 .register(REGISTERED_STREAM_ID, gid, connection.core.now);
21606 for chunk in body.chunks(QUEUED_RESPONSE_CHUNK_LEN) {
21607 context.streams[gid]
21608 .back
21609 .out
21610 .push_back(kawa::OutBlock::Store(kawa::Store::Static(chunk)));
21611 }
21612 assert_prepare_gate_is_shut(&context, gid);
21613 assert_eq!(
21614 connection.core.stream_table.expect_write(),
21615 None,
21616 "premise: no parked write, so the first pass takes the main loop"
21617 );
21618
21619 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
21620 let mut received = Vec::new();
21621 let mut ticks = 0usize;
21622
21623 loop {
21624 // What the event loop does: report the socket writable, run the
21625 // pass, and let the peer read whatever reached the kernel.
21626 connection.core.readiness.event.insert(Ready::WRITABLE);
21627 connection.writable(&mut context, EndpointClient(&mut router));
21628 std::thread::yield_now();
21629 drain_peer(&mut client, &mut peer, &mut received);
21630
21631 if received.len() >= body.len() {
21632 break;
21633 }
21634 ticks += 1;
21635 assert!(
21636 ticks < MAX_DRIVE_TICKS,
21637 "the response was truncated at {} of {} bytes after {ticks} \
21638 passes, with {} blocks still queued",
21639 received.len(),
21640 body.len(),
21641 context.streams[gid].back.out.len()
21642 );
21643 // Owed, not undelivered: once `kawa.out` is empty and rustls
21644 // holds nothing, every byte has been handed to the kernel and
21645 // `finalize_write` may legitimately quiesce while the last of it
21646 // is still crossing loopback. Asserting on `received` here would
21647 // blame the write machine for the harness's own race.
21648 let still_owed =
21649 !context.streams[gid].back.out.is_empty() || connection.socket.socket_wants_write();
21650 assert!(
21651 !still_owed || connection.core.readiness.interest.is_writable(),
21652 "a pass that still owes bytes must keep WRITABLE interest, or \
21653 no later event reaches this connection and the response is \
21654 truncated at {} of {} bytes with {} blocks queued",
21655 received.len(),
21656 body.len(),
21657 context.streams[gid].back.out.len()
21658 );
21659 }
21660
21661 assert_eq!(
21662 received.len(),
21663 body.len(),
21664 "the peer must receive every queued byte"
21665 );
21666 assert_eq!(
21667 received.as_slice(),
21668 body,
21669 "the peer must receive the queued bytes unchanged and in order"
21670 );
21671 assert!(
21672 context.streams[gid].back.out.is_empty(),
21673 "every queued block must be consumed once the response is delivered"
21674 );
21675 assert_eq!(
21676 context.streams[gid].metrics.bout,
21677 body.len(),
21678 "the stream must account exactly the bytes the socket took"
21679 );
21680 }
21681
21682 /// `force_disconnect`'s server arm, over the handler whose
21683 /// `socket_wants_write()` is the only production implementation that can
21684 /// answer `true`: it re-arms while rustls holds records and closes once
21685 /// they are gone.
21686 ///
21687 /// One test for both arms because the TRANSITION is the claim — the same
21688 /// connection, the same records, and the only thing that changes is
21689 /// whether the peer read them. A pair of connections would prove the two
21690 /// answers of a table `h2_close::force_disconnect_action` already tables
21691 /// exhaustively; what has never been proved is that this caller reads a
21692 /// real handler's answer at all.
21693 ///
21694 /// TO SEE THIS RED: in `ConnectionH2::force_disconnect`, replace
21695 /// `let tls_wants_write = self.tls_wants_write();` with
21696 /// `let tls_wants_write = false;` — the decision stops reading the socket,
21697 /// and the call closes the session while records are pending, which sends
21698 /// FIN and destroys them. This test fails on `a disconnect with records
21699 /// pending must not close the session: closing sends FIN and destroys the
21700 /// records rustls still holds, which the peer reads as a truncated
21701 /// response`. Measured: `1096 passed; 2 failed` — this test and
21702 /// `force_disconnect_re_arms_while_tls_records_are_pending`, the same
21703 /// decision with one witness over a modelled handler and one over the
21704 /// production one.
21705 #[test]
21706 fn force_disconnect_over_a_real_rustls_frontend_waits_for_the_records_to_drain() {
21707 let pool = make_pool_for_invariant_16();
21708 let (mut connection, mut peer, mut client) = rustls_h2_connection(&pool, H2State::GoAway);
21709 let body = queued_response_body();
21710
21711 let (absorbed, _status) = connection
21712 .socket
21713 .socket_write_vectored(&[IoSlice::new(body)]);
21714 assert!(
21715 absorbed > 0 && connection.socket.socket_wants_write(),
21716 "premise: the blocked kernel must leave real records pending"
21717 );
21718 assert!(
21719 !connection.core.peer_gone_after_final_goaway(),
21720 "premise: the peer must still be live, or the decision \
21721 short-circuits on its first boolean and never reads the socket"
21722 );
21723
21724 assert!(
21725 matches!(connection.force_disconnect(), MuxResult::Continue),
21726 "a disconnect with records pending must not close the session: \
21727 closing sends FIN and destroys the records rustls still holds, \
21728 which the peer reads as a truncated response"
21729 );
21730 assert!(
21731 connection.core.readiness.interest.is_writable(),
21732 "the delayed close must keep WRITABLE interest so the write path \
21733 can still flush"
21734 );
21735 // The records were written straight to the socket, outside any
21736 // writable pass, so no refusal is recorded and the delayed close
21737 // re-raises WRITABLE for the write path to run once more. A write the
21738 // same pass saw refused would leave it to the kernel's edge instead
21739 // (`a_refused_goaway_flush_waits_for_the_kernel_edge_then_closes`).
21740 assert!(
21741 connection.core.readiness.event.is_writable(),
21742 "with no refused write recorded, ensure_tls_flushed must re-signal \
21743 the WRITABLE event so the write path runs again"
21744 );
21745
21746 let mut received = Vec::new();
21747 assert!(
21748 flush_until_drained(
21749 &mut connection.socket,
21750 &mut client,
21751 &mut peer,
21752 &mut received
21753 ),
21754 "premise for the second arm: the records must actually drain once \
21755 the peer reads"
21756 );
21757
21758 assert!(
21759 matches!(connection.force_disconnect(), MuxResult::CloseSession),
21760 "with rustls holding nothing, the same call must close: delaying \
21761 further would strand a connection with no reason left to wait"
21762 );
21763 }
21764
21765 /// The `(H2State::Error, Position::Server)` arm of
21766 /// `ConnectionH2::dispatch_writable_state` reads the real handler's
21767 /// POST-flush answer: it keeps the session open while rustls still holds
21768 /// records — leaving WRITABLE to the kernel's edge, since the flush was
21769 /// refused — and closes once they are gone. The test drains the records
21770 /// itself between the two passes (`flush_until_drained`); the second pass
21771 /// only has to see that nothing is left.
21772 ///
21773 /// That arm has no flush of its own — `H2Shell::writable`'s preamble
21774 /// already issued this pass's `socket_write(&[])` and discarded both the
21775 /// size and the status it returned, and the `tls_wants_write` the arm is
21776 /// handed is the read after it, so that query is the only thing that can
21777 /// tell this site whether the flush landed. Until now the arm was
21778 /// exercised only as `h2_close::error_close_action`'s pure table; no test
21779 /// reached it through a connection whose handler could answer `true`,
21780 /// because none existed.
21781 ///
21782 /// TO SEE THIS RED: in that arm, replace
21783 /// `h2_close::error_close_action(tls_wants_write)` with
21784 /// `h2_close::error_close_action(false)` — the decision stops reading the
21785 /// socket and closes with records pending, sending FIN over plaintext the
21786 /// peer never received. Measured, this test fails on `a pass that finds
21787 /// records still pending after its own flush must not close the session:
21788 /// the close destroys plaintext the peer has not received and it reads the
21789 /// response as truncated`. Measured: `1116 passed; 1 failed` — it is the
21790 /// only test in the crate that moves.
21791 #[test]
21792 fn a_rustls_frontend_in_error_state_stays_open_until_its_records_drain() {
21793 let pool = make_pool_for_invariant_16();
21794 let (mut connection, mut peer, mut client) = rustls_h2_connection(&pool, H2State::Error);
21795 let mut context = test_context(&pool);
21796 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
21797 let body = queued_response_body();
21798
21799 let (absorbed, _status) = connection
21800 .socket
21801 .socket_write_vectored(&[IoSlice::new(body)]);
21802 assert!(
21803 absorbed > 0 && connection.socket.socket_wants_write(),
21804 "premise: the blocked kernel must leave real records pending, or \
21805 this arm reads its only input as false and the test proves nothing"
21806 );
21807
21808 connection.core.readiness.event.insert(Ready::WRITABLE);
21809 assert!(
21810 matches!(
21811 connection.writable(&mut context, EndpointClient(&mut router)),
21812 MuxResult::Continue
21813 ),
21814 "a pass that finds records still pending after its own flush must \
21815 not close the session: the close destroys plaintext the peer has \
21816 not received and it reads the response as truncated"
21817 );
21818 // The kernel refused the flush: it owes this socket its next
21819 // WRITABLE edge, so no synthetic one is queued (sozu-proxy/sozu#1788).
21820 assert!(
21821 connection.core.readiness.interest.is_writable(),
21822 "the delayed close must keep WRITABLE interest so the kernel edge \
21823 is delivered"
21824 );
21825 assert!(
21826 !connection.core.readiness.event.is_writable(),
21827 "a flush the kernel refused must leave WRITABLE to the kernel's \
21828 next edge, got {:?}",
21829 connection.core.readiness
21830 );
21831
21832 let mut received = Vec::new();
21833 assert!(
21834 flush_until_drained(
21835 &mut connection.socket,
21836 &mut client,
21837 &mut peer,
21838 &mut received
21839 ),
21840 "premise for the second arm: the records must actually drain once \
21841 the peer reads"
21842 );
21843
21844 connection.core.readiness.event.insert(Ready::WRITABLE);
21845 assert!(
21846 matches!(
21847 connection.writable(&mut context, EndpointClient(&mut router)),
21848 MuxResult::CloseSession
21849 ),
21850 "with rustls holding nothing, the same pass must close: the bytes \
21851 are all in the kernel and waiting longer strands the session"
21852 );
21853 }
21854
21855 /// The `H2State::GoAway` arm of `ConnectionH2::dispatch_writable_state`
21856 /// and the `ConnectionH2::dispatch_writable_state_after_flush` that
21857 /// settles it — the triple whose own comment calls it the primary
21858 /// truncation vector under HAProxy chaining — over the production TLS
21859 /// handler: it keeps the session open while rustls still holds records,
21860 /// leaving WRITABLE to the kernel's edge, and disconnects once they are
21861 /// gone. As in the Error-arm test, the records are drained by the test
21862 /// between the passes, not by the second pass.
21863 ///
21864 /// This is the triple #1454 names. Of the three, `finalize_write`'s is
21865 /// driven by `a_blocked_rustls_frontend_delivers_every_queued_byte_across_passes`
21866 /// above and the Error arm by
21867 /// `a_rustls_frontend_in_error_state_stays_open_until_its_records_drain`; this
21868 /// one completes the set. The fourth `socket_write(&[])` site,
21869 /// `flush_output_buffer`, is deliberately not targeted here: it keeps the
21870 /// status its flush returned instead of re-querying, so it is not this
21871 /// shape and a test aimed at it would prove nothing about the vector.
21872 ///
21873 /// The state assertion is not decoration. Falling through this arm reaches
21874 /// `force_disconnect`, which has a record guard of its own and answers
21875 /// `MuxResult::Continue` too — so the result alone cannot tell a correct
21876 /// hold from a fall-through that only looks correct. `H2State::Error` is
21877 /// what the fall-through leaves behind, and nothing else sets it here.
21878 ///
21879 /// TO SEE THIS RED: in `ConnectionH2::dispatch_writable_state_after_flush`,
21880 /// replace the `h2_close::goaway_close_action(TlsFlushPhase::AfterFlush,
21881 /// false, tls_wants_write)` third argument with `false`. The arm
21882 /// stops reading whether the flush `H2Shell::writable` performed for
21883 /// it landed, falls through to
21884 /// `force_disconnect`, and this test fails on `a GoAway pass whose flush
21885 /// did not land must stay in GoAway`. Measured: `1115 passed; 2 failed` —
21886 /// this test and `a_flush_that_does_not_drain_keeps_the_connection_open`,
21887 /// the same arm with one witness over a modelled handler and one over the
21888 /// production one.
21889 #[test]
21890 fn a_rustls_frontend_in_goaway_stays_open_until_its_records_drain() {
21891 let pool = make_pool_for_invariant_16();
21892 let (mut connection, mut peer, mut client) = rustls_h2_connection(&pool, H2State::GoAway);
21893 let mut context = test_context(&pool);
21894 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
21895 let body = queued_response_body();
21896
21897 let (absorbed, _status) = connection
21898 .socket
21899 .socket_write_vectored(&[IoSlice::new(body)]);
21900 assert!(
21901 absorbed > 0 && connection.socket.socket_wants_write(),
21902 "premise: the blocked kernel must leave real records pending"
21903 );
21904 assert!(
21905 !connection.core.peer_gone_after_final_goaway(),
21906 "premise: the peer must still be live, or the decision \
21907 short-circuits on its first boolean and never reads the socket"
21908 );
21909
21910 connection.core.readiness.event.insert(Ready::WRITABLE);
21911 assert!(
21912 matches!(
21913 connection.writable(&mut context, EndpointClient(&mut router)),
21914 MuxResult::Continue
21915 ),
21916 "a GoAway pass that finds records still pending after its own \
21917 flush must not close: FIN here destroys plaintext the peer never \
21918 received"
21919 );
21920 assert!(
21921 matches!(connection.core.state, H2State::GoAway),
21922 "a GoAway pass whose flush did not land must stay in GoAway: \
21923 reaching Error means the arm fell through to force_disconnect \
21924 and its own record guard, not this one, kept the session alive"
21925 );
21926 // The kernel refused the flush: it owes this socket its next
21927 // WRITABLE edge, so no synthetic one is queued (sozu-proxy/sozu#1788).
21928 assert!(
21929 !connection.core.readiness.event.is_writable(),
21930 "a flush the kernel refused must leave WRITABLE to the kernel's \
21931 next edge, got {:?}",
21932 connection.core.readiness
21933 );
21934
21935 let mut received = Vec::new();
21936 assert!(
21937 flush_until_drained(
21938 &mut connection.socket,
21939 &mut client,
21940 &mut peer,
21941 &mut received
21942 ),
21943 "premise for the second arm: the records must actually drain once \
21944 the peer reads"
21945 );
21946
21947 connection.core.readiness.event.insert(Ready::WRITABLE);
21948 assert!(
21949 matches!(
21950 connection.writable(&mut context, EndpointClient(&mut router)),
21951 MuxResult::CloseSession
21952 ),
21953 "with rustls holding nothing, the same pass must disconnect: every \
21954 byte is in the kernel and waiting longer strands the session"
21955 );
21956 }
21957
21958 // ── #1498: a DRIVEN TLS close_notify over a real `FrontRustls` ─────
21959 //
21960 // `H2Shell::begin_tls_close` is the seam that reaches
21961 // `FrontRustls::socket_close`, which is
21962 // `rustls::ServerConnection::send_close_notify`. Replacing its body with
21963 // `{}` at 0dddedf2 left the WHOLE suite green — `sozu-lib` `1114 passed;
21964 // 0 failed`, and no `sozu-e2e` test reddened on the mutation either —
21965 // while the same mutation on either of its two siblings from the same
21966 // extraction reddens at once. Measured against this branch's own `1115
21967 // passed` baseline: `tls_wants_write() -> false` gives `1104 passed; 11
21968 // failed` and a no-op `flush_tls_records()` gives `1110 passed; 5
21969 // failed`, ten and four of those respectively being tests OTHER than this
21970 // one — which is the asymmetry the whole file turns on. #1484's
21971 // coverage audit reached the same conclusion from the other direction,
21972 // listing `initiate_close_notify` among the `socket_wants_write` branches
21973 // its six `H2Shell<FrontRustls>` tests could not reach, for want of a
21974 // driven close_notify.
21975 //
21976 // The hole is structural rather than an oversight. Every handler the suite
21977 // drives a close over — `mio::net::TcpStream` and
21978 // `BackpressuredTlsSocket` — inherits `SocketHandler::socket_close`'s EMPTY
21979 // trait default, so to all of them the real seam and a no-op are the same
21980 // thing. `FrontRustls` is the only implementation in the tree that
21981 // overrides it, which is why the test below has to build one.
21982 //
21983 // And the assertion has to be on the ALERT, not on a FIN. That distinction
21984 // is the whole point of the alert: rustls answers `Ok(0)` from its reader
21985 // for a stream closed WITH a close_notify and
21986 // `UnexpectedEof("peer closed connection without sending TLS
21987 // close_notify")` for one closed without, so only the alert lets a peer
21988 // tell a complete response from a connection cut mid-stream. A test that
21989 // asserted a FIN had arrived would pass against the no-op mutant, which
21990 // still closes the socket.
21991
21992 /// The frontend idle timeout on a server H2 connection makes the peer
21993 /// receive a TLS close_notify alert.
21994 ///
21995 /// The route is the production one and the whole of it:
21996 /// `Mux::timeout` -> `Mux::delay_close_for_frontend_flush` ->
21997 /// `Connection::initiate_close_notify` ->
21998 /// `H2Shell::initiate_close_notify` -> `begin_tls_close` ->
21999 /// `FrontRustls::socket_close` -> `send_close_notify`.
22000 ///
22001 /// `Mux::delay_close_for_frontend_flush` is the SOLE caller of
22002 /// `Connection::initiate_close_notify`, and it is itself called from four
22003 /// sites in `Mux::ready_inner` and this one in `Mux::timeout_inner`.
22004 /// `Mux::shutting_down` is not among them: it drives frontend I/O and can
22005 /// flush records the alert has already generated, but it never asks for
22006 /// one. The timeout is chosen over the four `ready_inner` sites only
22007 /// because they need a `dyn ProxySession` and a `dyn L7Proxy`; the seam
22008 /// under test is the same on all five.
22009 ///
22010 /// The flush is `Connection::flush_output_buffer` — the same call
22011 /// `Mux::shutting_down_inner` makes for exactly this purpose, on a
22012 /// connection whose output queue is empty so nothing but the alert reaches
22013 /// the wire. `FrontRustls::socket_write(&[])` takes its "flush pending TLS
22014 /// records even if no application data was written" block, which is what
22015 /// hands the record to `write_tls`.
22016 ///
22017 /// TO SEE THIS RED: replace the body of `H2Shell::begin_tls_close`
22018 /// with `{}`. rustls is then never asked for the alert, `tls_wants_write()`
22019 /// answers false, `initiate_close_notify` returns false and the flush has
22020 /// nothing to push, so this test fails on `the peer must decrypt a TLS
22021 /// close_notify alert`. Measured: `1114 passed; 1 failed` — this test and
22022 /// nothing else, which is what a hole this complete looks like once it has
22023 /// exactly one witness. Note `close_notify_sent` is still set by the
22024 /// mutant, so no assertion on that flag could discriminate — only the
22025 /// bytes the peer receives can.
22026 #[test]
22027 fn a_frontend_timeout_makes_the_peer_receive_a_tls_close_notify() {
22028 use std::io::Read as _;
22029
22030 use crate::{StateResult, protocol::SessionState};
22031
22032 let pool = make_pool_for_invariant_16();
22033 let (mut connection, mut peer, mut client) = rustls_h2_connection(&pool, H2State::Header);
22034 // `Mux::consume_timer_entry` re-validates the wheel entry against the
22035 // core's own deadline and returns `Continue` having done NOTHING when
22036 // that deadline is still in the future. `ConnectionH2::new` arms one a
22037 // full `timeout_duration` out, so move it into the past: this is the
22038 // idle deadline genuinely elapsing, which is what the event loop would
22039 // be reacting to.
22040 connection.core.timeout_deadline = Some(Instant::now() - Duration::from_secs(60));
22041
22042 let frontend_token = mio::Token(0);
22043 let mut mux = crate::protocol::mux::Mux {
22044 configured_frontend_timeout: Duration::from_secs(30),
22045 frontend_token,
22046 frontend: crate::protocol::mux::Connection::H2(connection),
22047 router: Router::new(Duration::from_secs(30), Duration::from_secs(30)),
22048 // No streams at all, so the per-stream loop leaves `should_close`
22049 // true and `should_write` false, which is the only combination
22050 // that reaches `delay_close_for_frontend_flush`.
22051 context: test_context(&pool),
22052 session_ulid: Ulid::generate(),
22053 timeouts: crate::protocol::mux::MuxTimeouts::new(
22054 crate::timer::TimeoutContainer::new_empty(Duration::from_secs(30)),
22055 ),
22056 backend_registry: crate::protocol::mux::BackendRegistry::default(),
22057 backends: Rc::default(),
22058 };
22059 let mut metrics = SessionMetrics::new(None);
22060
22061 {
22062 let crate::protocol::mux::Connection::H2(h2) = &mux.frontend else {
22063 unreachable!("the frontend was built as H2")
22064 };
22065 assert!(
22066 !h2.socket.socket_wants_write(),
22067 "premise: the fixture must start with rustls holding nothing, \
22068 or the records this test attributes to the alert could be the \
22069 handshake's"
22070 );
22071 }
22072 assert!(
22073 !client
22074 .process_new_packets()
22075 .expect("the peer must decrypt what the frontend wrote")
22076 .peer_has_closed(),
22077 "premise: the peer must not have seen a close before the timeout \
22078 runs, or the closing assertion is reading a state it inherited"
22079 );
22080
22081 let result = mux.timeout(frontend_token, &mut metrics);
22082
22083 let mut received = Vec::new();
22084 let mut peer_saw_close_notify = false;
22085 for _ in 0..MAX_DRIVE_TICKS {
22086 mux.frontend.flush_output_buffer();
22087 // Loopback delivery is softirq-driven, not synchronous with
22088 // `write_tls`: give it a slot rather than spinning the CPU.
22089 std::thread::yield_now();
22090 drain_peer(&mut client, &mut peer, &mut received);
22091 peer_saw_close_notify = client
22092 .process_new_packets()
22093 .expect("the peer must decrypt what the frontend wrote")
22094 .peer_has_closed();
22095 if peer_saw_close_notify {
22096 break;
22097 }
22098 }
22099
22100 assert!(
22101 peer_saw_close_notify,
22102 "the peer must decrypt a TLS close_notify alert: without it a \
22103 client cannot tell this orderly shutdown from a stream truncated \
22104 mid-response, and reports a protocol error instead of a clean end"
22105 );
22106 let mut sentinel = [0u8; 1];
22107 assert!(
22108 matches!(client.reader().read(&mut sentinel), Ok(0)),
22109 "the alert must be what ended the peer's stream, stated in \
22110 rustls's own vocabulary: its reader answers Ok(0) only for a \
22111 stream closed with a close_notify, UnexpectedEof for a bare FIN \
22112 and WouldBlock for neither — so this is the assertion a FIN \
22113 cannot satisfy"
22114 );
22115 assert!(
22116 received.is_empty(),
22117 "nothing but the alert may reach the peer on this path: decrypted \
22118 plaintext here would mean the connection also wrote a frame after \
22119 asking rustls to close, which RFC 8446 §6.1 has the peer discard \
22120 ({} bytes)",
22121 received.len()
22122 );
22123 assert_eq!(
22124 result,
22125 StateResult::Continue,
22126 "a timeout that queued an alert must keep the session alive until \
22127 the records drain: closing here sends FIN over the very bytes the \
22128 close_notify was generated to deliver"
22129 );
22130 }
22131
22132 // ── Connection-level idle deadline: what counts as liveness ────────
22133 //
22134 // LIFECYCLE §9 invariant 9 says the connection timer resets only on
22135 // application activity, and `ConnectionH2::poll_read_target` spells out
22136 // the reason in prose: a peer sending periodic PINGs must not prevent
22137 // timeout detection on a stuck session. The READ side honoured it. The
22138 // WRITE side did not, because `H2Shell::write_streams` armed at pass
22139 // entry and `writable()` reaches that function on every proxying-state
22140 // pass — including the pass whose only output was the PING or SETTINGS
22141 // acknowledgement `flush_pending_control_frames` had just drained. So the
22142 // rule held in one direction and was undone in the other, for exactly the
22143 // frame types it names (sozu-proxy/sozu#1489).
22144 //
22145 // Every test below pairs the negative with a POSITIVE leg on the same
22146 // connection. A test that only asserted "the deadline did not move" would
22147 // pass just as well against a build that never armed at all, which is the
22148 // opposite defect and a worse one: it turns the frontend timeout into an
22149 // unconditional session cap.
22150
22151 /// Long enough that the per-stream idle deadline — a separate mechanism,
22152 /// deliberately untouched here — cannot retire the registered stream part
22153 /// way through a measurement and change what the later rows are measuring.
22154 const LIVENESS_TIMEOUT: Duration = Duration::from_secs(600);
22155
22156 /// A server connection whose frontend timeout and per-stream idle cap are
22157 /// both [`LIVENESS_TIMEOUT`], with ONE registered stream holding no queued
22158 /// response yet, plus the pass-zero instant every assertion below is
22159 /// expressed against.
22160 ///
22161 /// The stream is registered directly rather than opened with a HEADERS
22162 /// frame, and it starts EMPTY on purpose: a stream with queued output
22163 /// would let the write pass that flushes a PING ACK also flush that
22164 /// output, and arm the deadline for a reason the negative leg is not
22165 /// trying to measure.
22166 fn liveness_fixture(
22167 pool: &Rc<RefCell<Pool>>,
22168 ) -> (
22169 H2Shell<BackpressuredTlsSocket>,
22170 Context<TestListener>,
22171 GlobalStreamId,
22172 Instant,
22173 std::net::TcpStream,
22174 ) {
22175 let (mut connection, peer) = connection_with_backpressure(pool, 0, 0, H2State::Header);
22176 let t0 = connection.core.now;
22177 connection.core.timeout_duration = LIVENESS_TIMEOUT;
22178 connection.core.stream_idle_timeout = LIVENESS_TIMEOUT;
22179 connection.core.timeout_deadline = t0.checked_add(LIVENESS_TIMEOUT);
22180 connection
22181 .core
22182 .stream_table
22183 .set_expect_read(Some((H2StreamId::Zero, 9)));
22184 let mut context = test_context(pool);
22185 let gid = context
22186 .create_stream(Ulid::generate(), 1 << 16)
22187 .expect("test context must create a stream");
22188 connection
22189 .core
22190 .stream_table
22191 .register(REGISTERED_STREAM_ID, gid, connection.core.now);
22192 (connection, context, gid, t0, peer)
22193 }
22194
22195 /// A non-ACK PING. `ConnectionH2::handle_ping_frame` answers it by
22196 /// serialising a PING ACK into `self.zero` and parking
22197 /// `expect_write(Some(H2StreamId::Zero))`, which is what gives the write
22198 /// path something to send.
22199 fn liveness_ping_frame() -> Vec<u8> {
22200 let mut frame = vec![0, 0, 8, 6, 0, 0, 0, 0, 0];
22201 frame.extend_from_slice(&[1, 2, 3, 4, 5, 6, 7, 8]);
22202 frame
22203 }
22204
22205 /// A non-ACK SETTINGS carrying no entry.
22206 /// `ConnectionH2::handle_settings_frame` answers it with
22207 /// `serializer::SETTINGS_ACKNOWLEDGEMENT`, parking the same way.
22208 fn liveness_settings_frame() -> Vec<u8> {
22209 vec![0, 0, 0, 4, 0, 0, 0, 0, 0]
22210 }
22211
22212 /// A connection-level WINDOW_UPDATE. The control frame that queues NO
22213 /// acknowledgement, and therefore the one the pre-image already handled
22214 /// correctly — it is here so the fix is shown not to have moved it.
22215 fn liveness_window_update_frame() -> Vec<u8> {
22216 let mut frame = vec![0, 0, 4, 8, 0, 0, 0, 0, 0];
22217 frame.extend_from_slice(&1024u32.to_be_bytes());
22218 frame
22219 }
22220
22221 /// A HEADERS frame opening `stream_id`, END_STREAM | END_HEADERS.
22222 fn liveness_headers_frame(stream_id: u32) -> Vec<u8> {
22223 let mut encoder = crate::protocol::mux::hpack::Encoder::new();
22224 let block = encoder.encode([
22225 (&b":method"[..], &b"GET"[..]),
22226 (&b":scheme"[..], &b"https"[..]),
22227 (&b":authority"[..], &b"example.com"[..]),
22228 (&b":path"[..], &b"/"[..]),
22229 ]);
22230 let mut frame = Vec::with_capacity(9 + block.len());
22231 frame.extend_from_slice(&(block.len() as u32).to_be_bytes()[1..]);
22232 frame.push(1);
22233 frame.push(parser::FLAG_END_STREAM | parser::FLAG_END_HEADERS);
22234 frame.extend_from_slice(&stream_id.to_be_bytes());
22235 frame.extend_from_slice(&block);
22236 frame
22237 }
22238
22239 /// Spin until `n` bytes the peer wrote have reached the connection's
22240 /// socket.
22241 ///
22242 /// Not belt-and-braces: without it the read loop below can quiesce on a
22243 /// `WouldBlock` that only means "not yet", and every assertion downstream
22244 /// then measures a frame that was never delivered — which is a test that
22245 /// passes for the wrong reason, in the direction of the negative legs.
22246 fn liveness_await_bytes(connection: &H2Shell<BackpressuredTlsSocket>, n: usize) {
22247 let mut buf = vec![0u8; n];
22248 for _ in 0..1_000_000 {
22249 if let Ok(got) = connection.socket.socket_ref().peek(&mut buf)
22250 && got >= n
22251 {
22252 return;
22253 }
22254 std::thread::yield_now();
22255 }
22256 panic!("the peer's {n} bytes never reached the connection socket");
22257 }
22258
22259 /// Deliver `frame` at `at` and run the READ half only, the way
22260 /// `Mux::ready_inner` runs it: `readable()` while `filter_interest()` is
22261 /// readable.
22262 ///
22263 /// Stopping before the write half is what lets a test assert the premise —
22264 /// that the acknowledgement really was queued — instead of inferring it
22265 /// from the deadline it is about to measure.
22266 fn liveness_read(
22267 connection: &mut H2Shell<BackpressuredTlsSocket>,
22268 context: &mut Context<TestListener>,
22269 router: &mut Router,
22270 peer: &mut std::net::TcpStream,
22271 at: Instant,
22272 frame: &[u8],
22273 ) {
22274 context.now = at;
22275 peer.write_all(frame).expect("loopback write must complete");
22276 peer.flush().expect("loopback flush must complete");
22277 liveness_await_bytes(connection, frame.len());
22278 connection.core.readiness.interest.insert(Ready::READABLE);
22279 connection.core.readiness.event.insert(Ready::READABLE);
22280 for _ in 0..64 {
22281 if !connection.core.readiness.filter_interest().is_readable() {
22282 let mut leftover = [0u8; 1];
22283 assert!(
22284 !matches!(connection.socket.socket_ref().peek(&mut leftover), Ok(1)),
22285 "the read half stopped with the frame still unread, so \
22286 nothing below is measuring the frame it names"
22287 );
22288 return;
22289 }
22290 connection.readable(context, EndpointClient(&mut *router));
22291 }
22292 panic!(
22293 "the read half never settled: {:?}",
22294 connection.core.readiness
22295 );
22296 }
22297
22298 /// Run the WRITE half at the clock the read half left: `writable()` while
22299 /// `filter_interest()` is writable. This is the pass that used to arm the
22300 /// frontend deadline unconditionally.
22301 fn liveness_write(
22302 connection: &mut H2Shell<BackpressuredTlsSocket>,
22303 context: &mut Context<TestListener>,
22304 router: &mut Router,
22305 ) {
22306 for _ in 0..64 {
22307 if !connection.core.readiness.filter_interest().is_writable() {
22308 return;
22309 }
22310 connection.writable(context, EndpointClient(&mut *router));
22311 }
22312 panic!(
22313 "the write half never settled: {:?}",
22314 connection.core.readiness
22315 );
22316 }
22317
22318 /// Queue `blocks` as the registered stream's response and drive ONE
22319 /// `writable()` pass at `at`. Returns the bytes that reached the socket
22320 /// across the whole connection's lifetime so far.
22321 fn liveness_flush_stream(
22322 connection: &mut H2Shell<BackpressuredTlsSocket>,
22323 context: &mut Context<TestListener>,
22324 router: &mut Router,
22325 gid: GlobalStreamId,
22326 at: Instant,
22327 blocks: &[&'static [u8]],
22328 ) -> usize {
22329 for block in blocks {
22330 context.streams[gid]
22331 .back
22332 .out
22333 .push_back(kawa::OutBlock::Store(kawa::Store::Static(block)));
22334 }
22335 context.now = at;
22336 connection.core.readiness.interest.insert(Ready::WRITABLE);
22337 connection.core.readiness.event.insert(Ready::WRITABLE);
22338 connection.writable(context, EndpointClient(&mut *router));
22339 context.streams[gid].metrics.bout
22340 }
22341
22342 /// The deadline a connection publishes, as an offset from pass zero, so a
22343 /// failure reads in the units the issue's table is written in.
22344 fn liveness_deadline(connection: &H2Shell<BackpressuredTlsSocket>, t0: Instant) -> Duration {
22345 connection
22346 .core
22347 .poll_timeout()
22348 .expect("a live H2 connection must publish a deadline")
22349 .saturating_duration_since(t0)
22350 }
22351
22352 /// A peer that sends nothing but PINGs must not hold a stuck session open.
22353 ///
22354 /// The PING is answered — `handle_ping_frame` parks the ACK on
22355 /// `expect_write` and arms WRITABLE — so the write path genuinely runs and
22356 /// genuinely has bytes to emit. What it must not do is read that as the
22357 /// peer being alive on a stream: the ACK is the proxy talking to itself.
22358 ///
22359 /// TO SEE THIS RED: put `self.arm_timeout();` back as the first statement
22360 /// of `H2Shell::write_streams`. Measured on that revert, this test
22361 /// fails on `a PING is answered but is not stream activity`, with
22362 /// `left: 610s, right: 600.01s` — the deadline follows the PING instead of
22363 /// staying where the request left it. It is one of FOUR failures, the
22364 /// siblings being the three tests below; every other test in the suite
22365 /// passes.
22366 ///
22367 /// Neutering the OTHER half instead — `if size > 0` to `if false` in
22368 /// `ConnectionH2::handle_write` — reddens this test too, on its positive
22369 /// leg (`left: 600.01s, right: 1130s`). That is the pair that makes it a
22370 /// measurement: neither a build that always arms nor a build that never
22371 /// arms can satisfy it.
22372 #[test]
22373 fn a_ping_only_peer_does_not_postpone_the_connection_deadline() {
22374 let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 8, 16384)));
22375 let (mut connection, mut context, gid, t0, mut peer) = liveness_fixture(&pool);
22376 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
22377
22378 assert_eq!(
22379 liveness_deadline(&connection, t0),
22380 LIVENESS_TIMEOUT,
22381 "premise: the connection starts one full timeout out from pass zero"
22382 );
22383
22384 // The request. The READ side already honoured the rule, and this leg
22385 // is the control for it: a build that stopped arming on HEADERS would
22386 // make every "did not move" assertion below true for the wrong reason.
22387 let request_at = Duration::from_millis(10);
22388 liveness_read(
22389 &mut connection,
22390 &mut context,
22391 &mut router,
22392 &mut peer,
22393 t0 + request_at,
22394 &liveness_headers_frame(3),
22395 );
22396 liveness_write(&mut connection, &mut context, &mut router);
22397 assert_eq!(
22398 liveness_deadline(&connection, t0),
22399 request_at + LIVENESS_TIMEOUT,
22400 "HEADERS is application activity and must arm the deadline: \
22401 without this leg the assertions below cannot tell a fix from a \
22402 build that never arms at all"
22403 );
22404
22405 // The defect. Two PINGs, 490 seconds apart, either of which used to
22406 // renew the session on its own.
22407 for ping_at in [Duration::from_secs(10), Duration::from_secs(500)] {
22408 liveness_read(
22409 &mut connection,
22410 &mut context,
22411 &mut router,
22412 &mut peer,
22413 t0 + ping_at,
22414 &liveness_ping_frame(),
22415 );
22416 assert!(
22417 !connection.core.output.is_empty(),
22418 "premise: the PING must really have been answered and queued, \
22419 so the write pass below is the ACK-only pass this test is \
22420 about and not a no-op"
22421 );
22422
22423 liveness_write(&mut connection, &mut context, &mut router);
22424
22425 assert!(
22426 connection.core.output.is_empty(),
22427 "premise: the ACK flush must have completed, which is what \
22428 lets `flush_pending_control_frames` answer Proceed and fall \
22429 through into the proxying-state write pass"
22430 );
22431 assert_eq!(
22432 liveness_deadline(&connection, t0),
22433 request_at + LIVENESS_TIMEOUT,
22434 "a PING is answered but is not stream activity: the deadline \
22435 must stay where the request left it, or a peer sending \
22436 periodic PINGs holds a stuck session open indefinitely"
22437 );
22438 }
22439
22440 // A WINDOW_UPDATE queues no acknowledgement, so the pre-image already
22441 // left it alone. Pinned so the fix is shown not to have moved it.
22442 liveness_read(
22443 &mut connection,
22444 &mut context,
22445 &mut router,
22446 &mut peer,
22447 t0 + Duration::from_secs(520),
22448 &liveness_window_update_frame(),
22449 );
22450 liveness_write(&mut connection, &mut context, &mut router);
22451 assert_eq!(
22452 liveness_deadline(&connection, t0),
22453 request_at + LIVENESS_TIMEOUT,
22454 "a WINDOW_UPDATE queues nothing and must not arm the deadline"
22455 );
22456
22457 // The positive leg, on the same connection: outbound application data
22458 // IS activity. Without it every assertion above is satisfied by a
22459 // build that never arms the deadline at all.
22460 let response_at = Duration::from_secs(530);
22461 let written = liveness_flush_stream(
22462 &mut connection,
22463 &mut context,
22464 &mut router,
22465 gid,
22466 t0 + response_at,
22467 &[FIRST_BLOCK, SECOND_BLOCK],
22468 );
22469 assert_eq!(
22470 written, TOTAL_QUEUED,
22471 "premise for the positive leg: the pass must really have moved the \
22472 response bytes, or the deadline below moved without stream data"
22473 );
22474 assert_eq!(
22475 liveness_deadline(&connection, t0),
22476 response_at + LIVENESS_TIMEOUT,
22477 "a write pass that moved a stream's bytes IS liveness and must arm \
22478 the deadline: the fix withdraws arming from acknowledgement-only \
22479 passes, not from the write path"
22480 );
22481 }
22482
22483 /// The same contract for SETTINGS, the other control frame that queues an
22484 /// acknowledgement.
22485 ///
22486 /// Not a duplicate of the PING test: `handle_settings_frame` reaches the
22487 /// park through an entirely different body — the per-identifier apply
22488 /// loop, the `pending_table_size_update` signal and
22489 /// `serializer::SETTINGS_ACKNOWLEDGEMENT` rather than
22490 /// `serializer::gen_ping_acknowledgement` — and the issue's table measured
22491 /// both moving independently.
22492 ///
22493 /// TO SEE THIS RED: put `self.arm_timeout();` back as the first statement
22494 /// of `H2Shell::write_streams`. Measured on that revert, this test
22495 /// fails on `a SETTINGS ACK is not stream activity`, with
22496 /// `left: 640s, right: 600.01s`. Neutering the other half instead —
22497 /// `if size > 0` to `if false` in `ConnectionH2::handle_write` — reddens
22498 /// it on its positive leg, `left: 600.01s, right: 650s`.
22499 #[test]
22500 fn a_settings_only_peer_does_not_postpone_the_connection_deadline() {
22501 let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 8, 16384)));
22502 let (mut connection, mut context, gid, t0, mut peer) = liveness_fixture(&pool);
22503 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
22504
22505 let request_at = Duration::from_millis(10);
22506 liveness_read(
22507 &mut connection,
22508 &mut context,
22509 &mut router,
22510 &mut peer,
22511 t0 + request_at,
22512 &liveness_headers_frame(3),
22513 );
22514 liveness_write(&mut connection, &mut context, &mut router);
22515 assert_eq!(
22516 liveness_deadline(&connection, t0),
22517 request_at + LIVENESS_TIMEOUT,
22518 "premise: the request armed the deadline, so a later move is \
22519 attributable to the frame that caused it"
22520 );
22521
22522 liveness_read(
22523 &mut connection,
22524 &mut context,
22525 &mut router,
22526 &mut peer,
22527 t0 + Duration::from_secs(40),
22528 &liveness_settings_frame(),
22529 );
22530 assert!(
22531 !connection.core.output.is_empty(),
22532 "premise: the SETTINGS must really have been acknowledged and \
22533 queued, so the write pass below is the ACK-only pass"
22534 );
22535
22536 liveness_write(&mut connection, &mut context, &mut router);
22537
22538 assert!(
22539 connection.core.output.is_empty(),
22540 "premise: the ACK flush must have completed and fallen through \
22541 into the proxying-state write pass"
22542 );
22543 assert_eq!(
22544 liveness_deadline(&connection, t0),
22545 request_at + LIVENESS_TIMEOUT,
22546 "a SETTINGS ACK is not stream activity: the deadline must stay \
22547 where the request left it"
22548 );
22549
22550 let response_at = Duration::from_secs(50);
22551 let written = liveness_flush_stream(
22552 &mut connection,
22553 &mut context,
22554 &mut router,
22555 gid,
22556 t0 + response_at,
22557 &[FIRST_BLOCK, SECOND_BLOCK],
22558 );
22559 assert_eq!(
22560 written, TOTAL_QUEUED,
22561 "premise for the positive leg: the pass must really have moved the \
22562 response bytes"
22563 );
22564 assert_eq!(
22565 liveness_deadline(&connection, t0),
22566 response_at + LIVENESS_TIMEOUT,
22567 "outbound application data must still arm the deadline"
22568 );
22569 }
22570
22571 /// A write pass that emits a stream's bytes arms the deadline; the pass
22572 /// before it, which had nothing queued, does not.
22573 ///
22574 /// The PING and SETTINGS tests above each carry a positive leg, so this
22575 /// one is not the only guard against a build that never arms. What it adds
22576 /// is the pair measured on ONE connection with NOTHING but the queued
22577 /// response changing between the two passes: no frame is read, so neither
22578 /// `handle_headers_frame` nor `poll_read_target`'s DATA arm can be what
22579 /// moved the deadline, and the write path is the only remaining candidate.
22580 ///
22581 /// TO SEE THIS RED: change the `if size > 0` gate in
22582 /// `ConnectionH2::handle_write` to `if false`. The empty-pass assertion
22583 /// still holds and this test then fails on `a write pass that moved a
22584 /// stream's bytes must arm the deadline`, with `left: 600s, right: 620s`.
22585 /// Restoring `self.arm_timeout()` to the top of
22586 /// `H2Shell::write_streams` reddens the OTHER assertion instead, `a
22587 /// write pass that emitted nothing is not liveness`, with
22588 /// `left: 610s, right: 600s`.
22589 #[test]
22590 fn a_write_pass_that_moves_stream_bytes_arms_the_connection_deadline() {
22591 let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 8, 16384)));
22592 let (mut connection, mut context, gid, t0, _peer) = liveness_fixture(&pool);
22593 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
22594
22595 // A write pass with nothing queued: the control that makes the
22596 // assertion below a measurement of the bytes and not of the pass.
22597 let empty_at = Duration::from_secs(10);
22598 let written = liveness_flush_stream(
22599 &mut connection,
22600 &mut context,
22601 &mut router,
22602 gid,
22603 t0 + empty_at,
22604 &[],
22605 );
22606 assert_eq!(
22607 written, 0,
22608 "premise: a pass with nothing queued moves no byte"
22609 );
22610 assert_eq!(
22611 liveness_deadline(&connection, t0),
22612 LIVENESS_TIMEOUT,
22613 "a write pass that emitted nothing is not liveness"
22614 );
22615
22616 let response_at = Duration::from_secs(20);
22617 let written = liveness_flush_stream(
22618 &mut connection,
22619 &mut context,
22620 &mut router,
22621 gid,
22622 t0 + response_at,
22623 &[FIRST_BLOCK, SECOND_BLOCK],
22624 );
22625 assert_eq!(
22626 written, TOTAL_QUEUED,
22627 "premise: the pass must really have moved the response bytes"
22628 );
22629 assert_eq!(
22630 liveness_deadline(&connection, t0),
22631 response_at + LIVENESS_TIMEOUT,
22632 "a write pass that moved a stream's bytes must arm the deadline"
22633 );
22634 }
22635
22636 /// Draining changes nothing about what counts as liveness.
22637 ///
22638 /// `Mux::drive_frontend_shutdown_io` sets `force_h2_write` unconditionally
22639 /// for an H2 frontend, so a draining connection still in `H2State::Header`
22640 /// reaches `H2Shell::write_streams` on every `shutting_down()` poll —
22641 /// which is why arming at pass entry made a draining session immune to the
22642 /// frontend timeout for the whole drain, whether or not a byte moved. The
22643 /// drain keeps its deadline the same way every other pass does: by
22644 /// flushing a stream's bytes.
22645 ///
22646 /// TO SEE THIS RED: change the `if size > 0` gate in
22647 /// `ConnectionH2::handle_write` to `if false`. This test then fails on `a
22648 /// draining pass that flushed a stream's bytes must arm the deadline`,
22649 /// with `left: 600s, right: 620s`. Restoring `self.arm_timeout()` to the
22650 /// top of `H2Shell::write_streams` reddens the other assertion, `a
22651 /// draining pass that emitted nothing is not liveness either`, with
22652 /// `left: 610s, right: 600s`.
22653 #[test]
22654 fn a_draining_write_pass_that_moves_stream_bytes_still_arms_the_deadline() {
22655 let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 8, 16384)));
22656 let (mut connection, mut context, gid, t0, _peer) = liveness_fixture(&pool);
22657 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
22658 connection.core.drain.__test_set_draining();
22659
22660 let empty_at = Duration::from_secs(10);
22661 let written = liveness_flush_stream(
22662 &mut connection,
22663 &mut context,
22664 &mut router,
22665 gid,
22666 t0 + empty_at,
22667 &[],
22668 );
22669 assert_eq!(
22670 written, 0,
22671 "premise: a draining pass with nothing queued moves no byte"
22672 );
22673 assert_eq!(
22674 liveness_deadline(&connection, t0),
22675 LIVENESS_TIMEOUT,
22676 "a draining pass that emitted nothing is not liveness either: \
22677 `drive_frontend_shutdown_io` forces a write pass on every \
22678 shutting-down poll, so arming here renews the session for free"
22679 );
22680
22681 let response_at = Duration::from_secs(20);
22682 let written = liveness_flush_stream(
22683 &mut connection,
22684 &mut context,
22685 &mut router,
22686 gid,
22687 t0 + response_at,
22688 &[FIRST_BLOCK, SECOND_BLOCK],
22689 );
22690 assert_eq!(
22691 written, TOTAL_QUEUED,
22692 "premise: the draining pass must really have moved the response \
22693 bytes, so the drain has time to finish delivering them"
22694 );
22695 assert_eq!(
22696 liveness_deadline(&connection, t0),
22697 response_at + LIVENESS_TIMEOUT,
22698 "a draining pass that flushed a stream's bytes must arm the \
22699 deadline: the drain has to be able to finish delivering a \
22700 response the peer is still reading"
22701 );
22702 }
22703 // ── LIFECYCLE §9 invariant 14 — the backend accounting ledger ────────
22704 //
22705 // Invariant 14 is a balance: every stream removed outside
22706 // `Connection::end_stream` must release the `active_requests` charge its
22707 // creation took. Before #1340's Question 12 that balance was spread over
22708 // six sites, each reaching a `Rc<RefCell<Backend>>` through
22709 // `Position::Client` and mutating it in place, and nothing could observe
22710 // the pair — a missed decrement showed up as load-balancing drift in
22711 // production, days later.
22712 //
22713 // The core now emits `BackendDelta`s and the embedder performs them, so
22714 // the balance is a property of one observable list. These tests read that
22715 // list directly, and then apply it to a real `Backend` and assert the
22716 // counter returns to where it started, which is the invariant itself
22717 // rather than a proxy for it.
22718
22719 /// Sum a ledger into the net `active_requests` change it describes, and
22720 /// the number of connection releases it carries.
22721 fn reconcile(deltas: &[BackendDelta]) -> (i64, usize) {
22722 let mut net = 0i64;
22723 let mut closed = 0usize;
22724 for delta in deltas {
22725 match delta.change {
22726 BackendChange::StreamsStarted(count) => net += count as i64,
22727 BackendChange::StreamsEnded(count) => net -= count as i64,
22728 BackendChange::ConnectionClosed => closed += 1,
22729 // Only `Mux::dial_backend` emits it, and no scenario here
22730 // dials: an H2 connection's own paths never release a
22731 // reservation.
22732 BackendChange::DialFailed(_) => {
22733 unreachable!("no invariant-14 scenario dials a backend")
22734 }
22735 }
22736 }
22737 (net, closed)
22738 }
22739
22740 /// Everything one of these scenarios needs: a live backend connection in
22741 /// `Connected`, the registry entry behind it, and the peer that keeps the
22742 /// loopback socket open.
22743 struct LedgerFixture {
22744 connection: Connection<SessionTcpStream>,
22745 registry: crate::protocol::mux::BackendRegistry,
22746 backend: Rc<RefCell<crate::backends::Backend>>,
22747 _peer: std::net::TcpStream,
22748 }
22749
22750 fn ledger_fixture(pool: &Rc<RefCell<Pool>>) -> LedgerFixture {
22751 let (socket, peer) = connected_socket();
22752 let address: std::net::SocketAddr =
22753 "127.0.0.1:2".parse().expect("backend address must parse");
22754 let backend = Rc::new(RefCell::new(crate::backends::Backend::new(
22755 "ledger-backend",
22756 address,
22757 None,
22758 None,
22759 None,
22760 )));
22761 let mut registry = crate::protocol::mux::BackendRegistry::default();
22762 let backend_id = registry.id_for(&backend);
22763 let session_ulid = Ulid::generate();
22764 let mut connection = Connection::new_h2_client(
22765 session_ulid,
22766 SessionTcpStream::new(socket, session_ulid, Some(address)),
22767 "ledger-cluster".into(),
22768 backend_id,
22769 &mut PoolBufferSource::new(Rc::downgrade(pool)),
22770 Duration::from_secs(30),
22771 H2FloodConfig::default(),
22772 H2ConnectionConfig::default(),
22773 Duration::from_secs(30),
22774 None,
22775 )
22776 .expect("the test pool must hand out a buffer for the H2 backend");
22777 // `new_h2_client` opens in `Connecting`, where the charge is taken by
22778 // the embedder at the transition rather than by `start_stream`. Move
22779 // it the way a completed dial does, so these scenarios exercise the
22780 // `Connected` guard the three reset paths read.
22781 if let Position::Client(_, _, status) = connection.position_mut() {
22782 *status = BackendStatus::Connected;
22783 }
22784 LedgerFixture {
22785 connection,
22786 registry,
22787 backend,
22788 _peer: peer,
22789 }
22790 }
22791
22792 /// Open `count` streams on the backend through the production path, and
22793 /// return their global ids.
22794 fn open_streams(
22795 fixture: &mut LedgerFixture,
22796 context: &mut Context<TestListener>,
22797 count: usize,
22798 ) -> Vec<GlobalStreamId> {
22799 (0..count)
22800 .map(|_| {
22801 let gid = context
22802 .create_stream(Ulid::generate(), 65_535)
22803 .expect("the test pool must hand out stream buffers");
22804 assert!(
22805 fixture.connection.start_stream(gid, context),
22806 "the backend must accept the stream this scenario opens"
22807 );
22808 gid
22809 })
22810 .collect()
22811 }
22812
22813 /// Apply a ledger and read the counter it leaves behind.
22814 fn settle(fixture: &mut LedgerFixture, context: &mut Context<TestListener>) -> usize {
22815 let backends = RefCell::new(crate::backends::BackendMap::with_seed(0));
22816 for delta in std::mem::take(&mut context.backend_deltas) {
22817 fixture.registry.apply(delta, &backends);
22818 }
22819 fixture.backend.borrow().active_requests
22820 }
22821
22822 /// The nominal path: what `Connection::start_stream` charges,
22823 /// `Connection::end_stream` releases, and the close releases the
22824 /// connection slot exactly once.
22825 #[test]
22826 fn invariant_14_start_and_end_stream_balance() {
22827 let pool = make_pool_for_invariant_16();
22828 let mut context = test_context(&pool);
22829 let mut fixture = ledger_fixture(&pool);
22830 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
22831
22832 let gids = open_streams(&mut fixture, &mut context, 3);
22833 assert_eq!(
22834 reconcile(&context.backend_deltas),
22835 (3, 0),
22836 "three started streams must charge exactly three"
22837 );
22838
22839 for gid in gids {
22840 fixture.connection.end_stream(gid, &mut context);
22841 }
22842 fixture
22843 .connection
22844 .close(&mut context, EndpointClient(&mut router));
22845
22846 assert_eq!(
22847 reconcile(&context.backend_deltas),
22848 (0, 1),
22849 "every charge must be released, and the connection released once"
22850 );
22851 assert_eq!(
22852 settle(&mut fixture, &mut context),
22853 0,
22854 "applying the ledger must return active_requests to zero"
22855 );
22856 }
22857
22858 /// A refused `start_stream` must charge nothing at all — not charge and
22859 /// then correctly undo. The rollback that used to sit here is the shape
22860 /// this asserts is gone: a ledger with a `+1` and a `-1` in it would also
22861 /// reconcile to zero, so the assertion is on the list being EMPTY.
22862 #[test]
22863 fn invariant_14_a_refused_start_stream_charges_nothing() {
22864 let pool = make_pool_for_invariant_16();
22865 let mut context = test_context(&pool);
22866 let mut fixture = ledger_fixture(&pool);
22867
22868 // Drive the peer's MAX_CONCURRENT_STREAMS to zero so the very next
22869 // `start_stream` is refused by RFC 9113 §5.1.2's check.
22870 let Connection::H2(shell) = &mut fixture.connection else {
22871 unreachable!("the fixture was built as H2");
22872 };
22873 shell.core.peer_settings.settings_max_concurrent_streams = 0;
22874
22875 let gid = context
22876 .create_stream(Ulid::generate(), 65_535)
22877 .expect("the test pool must hand out stream buffers");
22878 assert!(
22879 !fixture.connection.start_stream(gid, &mut context),
22880 "premise: the backend must refuse this stream"
22881 );
22882
22883 assert!(
22884 context.backend_deltas.is_empty(),
22885 "a refused start_stream must leave no entry in the ledger at all, \
22886 not a charge and a compensating release: got {:?}",
22887 context.backend_deltas
22888 );
22889 assert_eq!(
22890 settle(&mut fixture, &mut context),
22891 0,
22892 "a refused start_stream must leave active_requests untouched"
22893 );
22894 }
22895
22896 /// An inbound `RST_STREAM` removes the stream without going through
22897 /// `Connection::end_stream`, so it owes the release itself.
22898 #[test]
22899 fn invariant_14_an_inbound_rst_stream_releases_its_charge() {
22900 let pool = make_pool_for_invariant_16();
22901 let mut context = test_context(&pool);
22902 let mut fixture = ledger_fixture(&pool);
22903 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
22904
22905 let _gids = open_streams(&mut fixture, &mut context, 2);
22906 let Connection::H2(shell) = &mut fixture.connection else {
22907 unreachable!("the fixture was built as H2");
22908 };
22909 let reset_id = *shell
22910 .core
22911 .stream_table
22912 .streams()
22913 .keys()
22914 .next()
22915 .expect("the scenario opened two streams");
22916 shell.core.handle_rst_stream_frame(
22917 parser::RstStream {
22918 stream_id: reset_id,
22919 error_code: H2Error::Cancel as u32,
22920 },
22921 &mut context,
22922 EndpointClient(&mut router),
22923 );
22924
22925 assert_eq!(
22926 reconcile(&context.backend_deltas),
22927 (1, 0),
22928 "the reset stream must release its charge, and only its own"
22929 );
22930 assert_eq!(
22931 settle(&mut fixture, &mut context),
22932 1,
22933 "one of the two streams is gone, so exactly one charge remains"
22934 );
22935 }
22936
22937 /// On a backend connection (`Position::Client`) a `RST_STREAM` the backend
22938 /// sends before its response starts — typically `REFUSED_STREAM` under
22939 /// load — is not the Rapid Reset signature: Sōzu opened the stream and the
22940 /// reset costs it nothing. Three hundred of them, sixty at a time, must
22941 /// not trip the pre-response reset cap.
22942 ///
22943 /// TO SEE THIS RED: in `ConnectionH2::handle_rst_stream_frame`, drop the
22944 /// `self.position.is_client()` term from `response_started`. Every reset
22945 /// then counts as pre-response, and with one reset per opened stream the
22946 /// 251st crosses `h2_max_rst_stream_abusive_lifetime`.
22947 #[test]
22948 fn backend_resets_before_the_response_are_not_rapid_reset() {
22949 const ROUNDS: usize = 5;
22950 const PER_ROUND: usize = 60;
22951 let pool = Rc::new(RefCell::new(Pool::with_capacity(
22952 4,
22953 2 * ROUNDS * PER_ROUND + 8,
22954 16_384,
22955 )));
22956 let mut context = test_context(&pool);
22957 let mut fixture = ledger_fixture(&pool);
22958 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
22959
22960 for round in 0..ROUNDS {
22961 let _gids = open_streams(&mut fixture, &mut context, PER_ROUND);
22962 let Connection::H2(shell) = &mut fixture.connection else {
22963 unreachable!("the fixture was built as H2");
22964 };
22965 let open: Vec<StreamId> = shell.core.stream_table.streams().keys().copied().collect();
22966 assert_eq!(open.len(), PER_ROUND, "round {round}: premise");
22967 for stream_id in open {
22968 let _ = shell.core.handle_rst_stream_frame(
22969 parser::RstStream {
22970 stream_id,
22971 error_code: H2Error::RefusedStream as u32,
22972 },
22973 &mut context,
22974 EndpointClient(&mut router),
22975 );
22976 assert!(
22977 !matches!(shell.core.state, H2State::GoAway | H2State::Error),
22978 "round {round}: backend reset of stream {stream_id} must not send GOAWAY"
22979 );
22980 }
22981 }
22982 }
22983
22984 // ── RST caps: what a frontend reset counts toward ───────────────────
22985
22986 /// A frontend H2 connection whose received-RST lifetime cap has the floor
22987 /// `lifetime_floor`, every other threshold at its default, so the
22988 /// scenarios below reach the cap in a handful of resets instead of
22989 /// 200 000.
22990 fn frontend_rst_fixture(
22991 lifetime_floor: u64,
22992 ) -> (
22993 Rc<RefCell<Pool>>,
22994 H2Shell<mio::net::TcpStream>,
22995 std::net::TcpStream,
22996 Context<TestListener>,
22997 Router,
22998 ) {
22999 let pool = Rc::new(RefCell::new(Pool::with_capacity(8, 512, 16_384)));
23000 let (mut connection, peer) = test_h2_connection(&pool, None);
23001 let config = H2FloodConfig::from_optional(
23002 None,
23003 None,
23004 None,
23005 None,
23006 None,
23007 None,
23008 None,
23009 Some(lifetime_floor),
23010 None,
23011 None,
23012 None,
23013 None,
23014 None,
23015 None,
23016 );
23017 connection.core.flood_detector =
23018 h2_flood_detector::H2FloodDetector::new(config, connection.core.now);
23019 let context = test_context(&pool);
23020 let router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
23021 (pool, connection, peer, context, router)
23022 }
23023
23024 /// Register client stream `stream_id` on `connection` as a stream routed
23025 /// to a backend, with its response started or not.
23026 fn open_routed_frontend_stream(
23027 connection: &mut H2Shell<mio::net::TcpStream>,
23028 context: &mut Context<TestListener>,
23029 stream_id: StreamId,
23030 response_started: bool,
23031 ) {
23032 let gid = context
23033 .create_stream(Ulid::generate(), 65_535)
23034 .expect("the test pool must hand out stream buffers");
23035 let stream = &mut context.streams[gid];
23036 stream.context.backend_id = Some(Rc::from("probe-backend"));
23037 if response_started {
23038 stream.back.parsing_phase = kawa::ParsingPhase::Body;
23039 }
23040 let now = connection.core.now;
23041 connection.core.stream_table.register(stream_id, gid, now);
23042 }
23043
23044 /// Feed one client `RST_STREAM(CANCEL)` for `stream_id` through
23045 /// `handle_rst_stream_frame`, and report whether the connection is now
23046 /// going away.
23047 fn client_cancels(
23048 connection: &mut H2Shell<mio::net::TcpStream>,
23049 context: &mut Context<TestListener>,
23050 router: &mut Router,
23051 stream_id: StreamId,
23052 ) -> bool {
23053 let _ = connection.core.handle_rst_stream_frame(
23054 parser::RstStream {
23055 stream_id,
23056 error_code: H2Error::Cancel as u32,
23057 },
23058 context,
23059 EndpointClient(router),
23060 );
23061 matches!(connection.core.state, H2State::GoAway | H2State::Error)
23062 }
23063
23064 /// A client that cancels every stream after its response started — a
23065 /// gRPC client abandoning server streams, a browser leaving a page while
23066 /// downloads run — resets exactly as many streams as a backend answered.
23067 /// The received-RST lifetime cap must never trip on it.
23068 ///
23069 /// TO SEE THIS RED: in `ConnectionH2::handle_rst_stream_frame`, count the
23070 /// reset stream with `record_stream_opened` after `record_rst_lifetime`
23071 /// instead of before. Each check then sees one more reset than answered
23072 /// streams, and the reset just past the floor sends GOAWAY.
23073 #[test]
23074 fn resets_after_the_response_never_trip_the_lifetime_cap() {
23075 let (_pool, mut connection, _peer, mut context, mut router) = frontend_rst_fixture(4);
23076 for n in 0..64u32 {
23077 let stream_id = 2 * n + 1;
23078 open_routed_frontend_stream(&mut connection, &mut context, stream_id, true);
23079 assert!(
23080 !client_cancels(&mut connection, &mut context, &mut router, stream_id),
23081 "reset {} of a stream whose response started must not send GOAWAY",
23082 n + 1
23083 );
23084 }
23085 }
23086
23087 /// A frontend connection whose pre-response cap has the floor `floor`,
23088 /// every other threshold at its default (soft refusal at 50 %).
23089 fn frontend_pre_response_fixture(
23090 floor: u64,
23091 ) -> (
23092 Rc<RefCell<Pool>>,
23093 H2Shell<mio::net::TcpStream>,
23094 std::net::TcpStream,
23095 Context<TestListener>,
23096 Router,
23097 ) {
23098 let pool = Rc::new(RefCell::new(Pool::with_capacity(8, 512, 16_384)));
23099 let (mut connection, peer) = test_h2_connection(&pool, None);
23100 let config = H2FloodConfig::from_optional(
23101 None,
23102 None,
23103 None,
23104 None,
23105 None,
23106 None,
23107 None,
23108 None,
23109 Some(floor),
23110 None,
23111 None,
23112 None,
23113 None,
23114 None,
23115 );
23116 connection.core.flood_detector =
23117 h2_flood_detector::H2FloodDetector::new(config, connection.core.now);
23118 let context = test_context(&pool);
23119 let router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
23120 (pool, connection, peer, context, router)
23121 }
23122
23123 /// A client that ignores `REFUSED_STREAM` and resets the streams Sōzu
23124 /// refused under flood pressure still reaches the pre-response cap: a
23125 /// refused stream never got a response, so its reset is a pre-response
23126 /// one even though the stream was never registered. A reset of a stream
23127 /// that is simply gone keeps counting as response-started.
23128 ///
23129 /// TO SEE THIS RED: in `ConnectionH2::handle_rst_stream_frame`, delete
23130 /// the `flood_refused_streams` arm. The resets then count as
23131 /// response-started, the pre-response count stops at the soft threshold,
23132 /// and no GOAWAY is ever sent.
23133 #[test]
23134 fn resets_of_streams_refused_under_flood_count_as_pre_response() {
23135 const FLOOR: u64 = 10;
23136 let (_pool, mut connection, _peer, mut context, mut router) =
23137 frontend_pre_response_fixture(FLOOR);
23138 let refused = (FLOOR as u32 + 1) * 2;
23139 for n in 0..refused {
23140 let stream_id = 2 * n + 1;
23141 connection
23142 .core
23143 .stream_table
23144 .observe_peer_stream_id(stream_id);
23145 connection.core.note_flood_refused_stream(stream_id);
23146 }
23147 assert_eq!(
23148 connection.core.flood_refused_streams,
23149 Some((1, 2 * refused - 1)),
23150 "consecutive refusals form one run"
23151 );
23152 // Resets of ids past the run: gone, not refused, never pre-response,
23153 // and no reason to stop refusing.
23154 for n in 0..=FLOOR as u32 {
23155 let stream_id = 2 * (refused + n) + 1;
23156 assert!(
23157 !client_cancels(&mut connection, &mut context, &mut router, stream_id),
23158 "a reset of a stream outside the refused run must not count as pre-response"
23159 );
23160 }
23161 assert!(!connection.core.flood_refusal_ignored);
23162 let mut tripped_at = None;
23163 for n in 0..refused {
23164 if client_cancels(&mut connection, &mut context, &mut router, 2 * n + 1) {
23165 tripped_at = Some(u64::from(n) + 1);
23166 break;
23167 }
23168 }
23169 assert_eq!(
23170 tripped_at,
23171 Some(FLOOR + 1),
23172 "resets of refused streams must reach the pre-response cap just past its floor"
23173 );
23174 assert!(
23175 connection.core.flood_refusal_ignored,
23176 "a client that resets a refused stream gets no more refusals"
23177 );
23178 }
23179
23180 /// An accepted stream ends the refused run; the next refusal starts a new
23181 /// one, so a stream that was served never counts as refused.
23182 #[test]
23183 fn an_accepted_stream_ends_the_flood_refused_run() {
23184 let (_pool, mut connection, _peer, mut context, _router) =
23185 frontend_pre_response_fixture(10);
23186 connection.core.note_flood_refused_stream(1);
23187 connection.core.note_flood_refused_stream(3);
23188 assert_eq!(connection.core.flood_refused_streams, Some((1, 3)));
23189 connection
23190 .core
23191 .create_stream(5, &mut context)
23192 .expect("the pool must hand out stream buffers");
23193 connection.core.note_flood_refused_stream(7);
23194 assert_eq!(
23195 connection.core.flood_refused_streams,
23196 Some((7, 7)),
23197 "a refusal after an accepted stream starts a new run"
23198 );
23199 }
23200
23201 /// A frontend connection over a [`PacedSocket`] with streams 1 and 3
23202 /// open, a pre-response floor of 10 and a glitch limit of `glitch_limit`,
23203 /// its SETTINGS acknowledged, and `pre_response_resets` pre-response
23204 /// resets on record. Six or more put it in the soft state: past half the
23205 /// floor, more than the streams it got answered (none).
23206 fn soft_refusal_connection(
23207 glitch_limit: Option<u32>,
23208 pre_response_resets: u32,
23209 ) -> (
23210 Rc<RefCell<Pool>>,
23211 H2Shell<PacedSocket>,
23212 Context<TestListener>,
23213 Router,
23214 std::net::TcpStream,
23215 ) {
23216 let (pool, mut connection, context, router, peer) = two_requests_read(usize::MAX);
23217 connection.core.flood_detector = h2_flood_detector::H2FloodDetector::new(
23218 H2FloodConfig::from_optional(
23219 None,
23220 None,
23221 None,
23222 None,
23223 None,
23224 None,
23225 glitch_limit,
23226 None,
23227 Some(10),
23228 None,
23229 None,
23230 None,
23231 None,
23232 None,
23233 ),
23234 connection.core.now,
23235 );
23236 connection.core.settings_sent_at = None;
23237 for _ in 0..pre_response_resets {
23238 assert!(
23239 connection
23240 .core
23241 .flood_detector
23242 .record_rst_received(false, false, connection.core.now)
23243 .is_none(),
23244 "premise: the resets on record stay under the cap"
23245 );
23246 }
23247 (pool, connection, context, router, peer)
23248 }
23249
23250 /// A complete `GET /` HEADERS frame on `stream_id`, from the static
23251 /// table and literals without indexing, so it decodes whatever the
23252 /// peer's dynamic table holds.
23253 fn static_get_headers(stream_id: StreamId) -> Vec<u8> {
23254 let mut block = vec![0x82, 0x84, 0x87, 0x01, 11];
23255 block.extend_from_slice(b"example.com");
23256 orphan_frame(
23257 1,
23258 parser::FLAG_END_STREAM | parser::FLAG_END_HEADERS,
23259 stream_id,
23260 block.len() as u32,
23261 &block,
23262 )
23263 }
23264
23265 /// Feed `inbound` to `connection`, flush what it answers, and return
23266 /// every frame on the wire so far.
23267 fn frames_after(
23268 connection: &mut H2Shell<PacedSocket>,
23269 context: &mut Context<TestListener>,
23270 router: &mut Router,
23271 inbound: &[u8],
23272 ) -> Vec<PeerFrame> {
23273 connection.socket.inbound.extend(inbound);
23274 // A pass may stop after a frame that queued output, so drive more
23275 // passes than there are frames.
23276 for _ in 0..256 {
23277 connection.core.readiness.event.insert(Ready::READABLE);
23278 if connection.core.readiness.filter_interest().is_readable() {
23279 connection.readable(context, EndpointClient(router));
23280 }
23281 connection.core.readiness.event.insert(Ready::WRITABLE);
23282 if connection.core.readiness.filter_interest().is_writable() {
23283 connection.writable(context, EndpointClient(router));
23284 }
23285 }
23286 peer_frames(&connection.socket.wire).expect("whole frames on the wire")
23287 }
23288
23289 /// The error code of the first frame of type `kind` (3 RST_STREAM, 7
23290 /// GOAWAY) on `stream_id` (0 for GOAWAY) in `frames`.
23291 fn error_code_of(frames: &[PeerFrame], kind: u8, stream_id: StreamId) -> Option<u32> {
23292 let at = if kind == 7 { 4 } else { 0 };
23293 frames
23294 .iter()
23295 .find(|(k, _, sid, _)| *k == kind && *sid == stream_id)
23296 .map(|(_, _, _, payload)| {
23297 u32::from_be_bytes(payload[at..at + 4].try_into().expect("an error code"))
23298 })
23299 }
23300
23301 /// RFC 9113 §5.1.1: a new stream's id must be greater than every id the
23302 /// client already used, refused ones included. After stream 65 is refused
23303 /// under flood pressure, HEADERS on 55 opens no stream: it is a
23304 /// connection error of type PROTOCOL_ERROR, and the refused run stays
23305 /// `(65, 65)`.
23306 ///
23307 /// TO SEE THIS RED: in `ConnectionH2::handle_header_state`, compare a new
23308 /// client stream's id with `last_stream_id` only, as before the fix. Stream
23309 /// 55 is then taken for a new stream and refused too, and the debug build
23310 /// panics with `the refused run ends at the stream just refused`.
23311 #[test]
23312 fn headers_below_a_refused_stream_id_is_a_protocol_error() {
23313 let (_pool, mut connection, mut context, mut router, _peer) =
23314 soft_refusal_connection(None, 6);
23315 let mut wire = static_get_headers(65);
23316 wire.extend(static_get_headers(55));
23317 let frames = frames_after(&mut connection, &mut context, &mut router, &wire);
23318 assert_eq!(
23319 error_code_of(&frames, 3, 65),
23320 Some(H2Error::RefusedStream as u32),
23321 "premise: stream 65 is refused under flood pressure, got {frames:?}"
23322 );
23323 assert_eq!(
23324 error_code_of(&frames, 7, 0),
23325 Some(H2Error::ProtocolError as u32),
23326 "HEADERS on an id below a refused one must end the connection with \
23327 GOAWAY(PROTOCOL_ERROR), got {frames:?}"
23328 );
23329 assert_eq!(
23330 error_code_of(&frames, 3, 55),
23331 None,
23332 "stream 55 must not be refused as a new stream"
23333 );
23334 assert_eq!(connection.core.flood_refused_streams, Some((65, 65)));
23335 }
23336
23337 /// Soft refusals count toward the glitch budget once the client
23338 /// acknowledged Sōzu's SETTINGS, like the draining and
23339 /// SETTINGS_MAX_CONCURRENT_STREAMS refusals: a client that keeps opening
23340 /// streams it is refused, without resetting any, ends with
23341 /// GOAWAY(ENHANCE_YOUR_CALM).
23342 ///
23343 /// TO SEE THIS RED: in `ConnectionH2::handle_header_state`, drop the
23344 /// `record_glitch` call of the soft-refusal branch. Every stream is then
23345 /// refused and no GOAWAY ever comes.
23346 #[test]
23347 fn soft_refusals_count_toward_the_glitch_budget() {
23348 const GLITCH_LIMIT: u32 = 20;
23349 let (_pool, mut connection, mut context, mut router, _peer) =
23350 soft_refusal_connection(Some(GLITCH_LIMIT), 6);
23351 let mut wire = Vec::new();
23352 for n in 0..2 * GLITCH_LIMIT {
23353 wire.extend(static_get_headers(5 + 2 * n));
23354 }
23355 let frames = frames_after(&mut connection, &mut context, &mut router, &wire);
23356 assert_eq!(
23357 error_code_of(&frames, 3, 5),
23358 Some(H2Error::RefusedStream as u32),
23359 "premise: the first new stream is refused under flood pressure"
23360 );
23361 assert_eq!(
23362 error_code_of(&frames, 7, 0),
23363 Some(H2Error::EnhanceYourCalm as u32),
23364 "a client that keeps opening refused streams must exhaust the glitch \
23365 budget, got {} RST_STREAM frames and no GOAWAY",
23366 frames.iter().filter(|(kind, ..)| *kind == 3).count()
23367 );
23368 assert!(
23369 frames.iter().filter(|(kind, ..)| *kind == 3).count() <= GLITCH_LIMIT as usize + 1,
23370 "the glitch budget bounds the refusals"
23371 );
23372 }
23373
23374 /// A RST_STREAM the client sends on the wire for a stream refused under
23375 /// flood pressure goes through the closed-stream branch of
23376 /// `ConnectionH2::handle_header_state`, which counts one glitch, and then
23377 /// reaches `ConnectionH2::handle_rst_stream_frame`: refusals stop for the
23378 /// connection, so its next stream is opened, and the reset counts as a
23379 /// pre-response one, so with ten pre-response resets on record it
23380 /// trips the cap's floor of 10.
23381 ///
23382 /// TO SEE THIS RED: in `ConnectionH2::handle_rst_stream_frame`, delete
23383 /// the `flood_refused_streams` arm: `a client that resets a refused
23384 /// stream gets no more refusals` fails, stream 7 is refused, and the
23385 /// reset counts as response-started, so no GOAWAY comes.
23386 #[test]
23387 fn a_client_reset_of_a_refused_stream_on_the_wire_ends_the_refusals() {
23388 let rst = |stream_id: StreamId| {
23389 orphan_frame(3, 0, stream_id, 4, &(H2Error::Cancel as u32).to_be_bytes())
23390 };
23391
23392 let (_pool, mut connection, mut context, mut router, _peer) =
23393 soft_refusal_connection(None, 6);
23394 let frames = frames_after(
23395 &mut connection,
23396 &mut context,
23397 &mut router,
23398 &static_get_headers(5),
23399 );
23400 assert_eq!(
23401 error_code_of(&frames, 3, 5),
23402 Some(H2Error::RefusedStream as u32),
23403 "premise: stream 5 is refused under flood pressure"
23404 );
23405 let glitches = connection.core.flood_detector.glitch_count();
23406 frames_after(&mut connection, &mut context, &mut router, &rst(5));
23407 assert_eq!(
23408 connection.core.flood_detector.glitch_count(),
23409 glitches + 1,
23410 "the reset of a refused stream costs one glitch"
23411 );
23412 assert!(
23413 connection.core.flood_refusal_ignored,
23414 "a client that resets a refused stream gets no more refusals"
23415 );
23416 let frames = frames_after(
23417 &mut connection,
23418 &mut context,
23419 &mut router,
23420 &static_get_headers(7),
23421 );
23422 assert_eq!(
23423 error_code_of(&frames, 3, 7),
23424 None,
23425 "no stream is refused after the client reset a refused one"
23426 );
23427 assert!(
23428 connection.core.stream_table.get(7).is_some(),
23429 "stream 7 is opened"
23430 );
23431
23432 // Ten pre-response resets on record: the reset of a refused stream is
23433 // the eleventh, past the floor.
23434 let (_pool, mut connection, mut context, mut router, _peer) =
23435 soft_refusal_connection(None, 10);
23436 let mut wire = static_get_headers(5);
23437 wire.extend(rst(5));
23438 let frames = frames_after(&mut connection, &mut context, &mut router, &wire);
23439 assert_eq!(
23440 error_code_of(&frames, 3, 5),
23441 Some(H2Error::RefusedStream as u32),
23442 "premise: stream 5 is refused under flood pressure"
23443 );
23444 assert_eq!(
23445 error_code_of(&frames, 7, 0),
23446 Some(H2Error::EnhanceYourCalm as u32),
23447 "the reset of a refused stream must count as pre-response, got {frames:?}"
23448 );
23449 }
23450
23451 /// HEADERS without END_STREAM, `len` bytes of DATA split in frames of at
23452 /// most 16 KiB, and a trailer section with END_STREAM on `stream_id`:
23453 /// what a client streaming an upload with request trailers sends before
23454 /// it reads anything back.
23455 fn upload_with_trailers(stream_id: StreamId, len: usize) -> Vec<u8> {
23456 let mut block = vec![0x83, 0x84, 0x87, 0x01, 11];
23457 block.extend_from_slice(b"example.com");
23458 let mut wire = orphan_frame(
23459 1,
23460 parser::FLAG_END_HEADERS,
23461 stream_id,
23462 block.len() as u32,
23463 &block,
23464 );
23465 wire.extend(refused_stream_data(stream_id, len, 0));
23466 // `x-t: 1`, a literal with incremental indexing and a new name: it
23467 // becomes the newest dynamic table entry, index 62 (RFC 7541 §6.2.1).
23468 let trailers = [0x40, 3, b'x', b'-', b't', 1, b'1'];
23469 wire.extend(orphan_frame(
23470 1,
23471 parser::FLAG_END_HEADERS | parser::FLAG_END_STREAM,
23472 stream_id,
23473 trailers.len() as u32,
23474 &trailers,
23475 ));
23476 wire
23477 }
23478
23479 /// `len` bytes of DATA on `stream_id` in frames of at most 16 KiB, the
23480 /// last one carrying `last_flags`.
23481 fn refused_stream_data(stream_id: StreamId, len: usize, last_flags: u8) -> Vec<u8> {
23482 let mut wire = Vec::new();
23483 let mut left = len;
23484 while left > 0 {
23485 let size = left.min(16_384);
23486 left -= size;
23487 let flags = if left == 0 { last_flags } else { 0 };
23488 wire.extend(orphan_frame(
23489 0,
23490 flags,
23491 stream_id,
23492 size as u32,
23493 &vec![b'x'; size],
23494 ));
23495 }
23496 wire
23497 }
23498
23499 /// RFC 9113 §5.1: the frames a client sent on a stream before it read
23500 /// Sōzu's `REFUSED_STREAM` are ignored, like those on a stream Sōzu
23501 /// reset. A refused upload carrying request trailers costs the client
23502 /// that stream only: no `RST_STREAM(STREAM_CLOSED)` for its DATA, no
23503 /// GOAWAY for its trailers, and the streams already open, and the
23504 /// connection, carry on.
23505 ///
23506 /// TO SEE THIS RED: in `ConnectionH2::handle_header_state`, drop the
23507 /// `remember_refused` call of the soft-refusal branch. The DATA
23508 /// frame then takes the closed-stream branch, and the trailers end the
23509 /// connection with GOAWAY(PROTOCOL_ERROR).
23510 #[test]
23511 fn trailers_on_a_refused_stream_are_ignored() {
23512 let (_pool, mut connection, mut context, mut router, _peer) =
23513 soft_refusal_connection(None, 6);
23514 let glitches = connection.core.flood_detector.glitch_count();
23515 let frames = frames_after(
23516 &mut connection,
23517 &mut context,
23518 &mut router,
23519 &upload_with_trailers(5, 1000),
23520 );
23521 assert_eq!(
23522 error_code_of(&frames, 3, 5),
23523 Some(H2Error::RefusedStream as u32),
23524 "premise: stream 5 is refused under flood pressure, got {frames:?}"
23525 );
23526 assert_eq!(
23527 frames
23528 .iter()
23529 .filter(|(kind, _, sid, _)| *kind == 3 && *sid == 5)
23530 .count(),
23531 1,
23532 "the refused stream gets its REFUSED_STREAM and nothing more, got {frames:?}"
23533 );
23534 assert_eq!(
23535 error_code_of(&frames, 7, 0),
23536 None,
23537 "frames on a refused stream must not end the connection, got {frames:?}"
23538 );
23539 assert_eq!(
23540 connection.core.flood_detector.glitch_count(),
23541 glitches + 1,
23542 "only the refusal itself costs a glitch"
23543 );
23544 assert!(
23545 connection.core.stream_table.get(1).is_some()
23546 && connection.core.stream_table.get(3).is_some(),
23547 "the streams already open survive"
23548 );
23549 assert!(
23550 !matches!(connection.core.state, H2State::GoAway | H2State::Error),
23551 "the connection survives, got {:?}",
23552 connection.core.state
23553 );
23554 // The ignored trailer block was still decoded (RFC 9113 §4.3): the
23555 // entry it indexed is the newest in the connection's dynamic table.
23556 let mut decoded = Vec::new();
23557 let status = connection
23558 .core
23559 .hpack
23560 .decoder_mut()
23561 .decode_with_cb(&[0x80 | 62], |k, v| {
23562 decoded.push((k.into_owned(), v.into_owned()));
23563 });
23564 assert!(status.is_ok(), "index 62 must resolve, got {status:?}");
23565 assert_eq!(
23566 decoded,
23567 vec![(b"x-t".to_vec(), b"1".to_vec())],
23568 "the ignored trailers must keep the HPACK decoder in step with the peer"
23569 );
23570 // The connection reads on: a later PING is answered.
23571 let frames = frames_after(&mut connection, &mut context, &mut router, &orphan_ping());
23572 assert!(
23573 frames.iter().any(|(kind, flags, _, payload)| *kind == 6
23574 && *flags & 1 == 1
23575 && payload == b"pingpong"),
23576 "the connection must keep reading after the ignored frames, got {frames:?}"
23577 );
23578 }
23579
23580 /// RFC 9113 §5.1 and §6.9: DATA a client sent on a refused stream within
23581 /// the stream's initial window — a browser upload filling it in 16 KiB
23582 /// frames — is ignored without a glitch or a `RST_STREAM(STREAM_CLOSED)`
23583 /// per frame, and still counts toward the connection's receive window,
23584 /// which Sōzu credits back.
23585 ///
23586 /// TO SEE THIS RED: in `ConnectionH2::handle_header_state`, drop the
23587 /// `remember_refused` call of the soft-refusal branch. Each of
23588 /// the four DATA frames then costs a glitch and a RST_STREAM.
23589 #[test]
23590 fn data_on_a_refused_stream_is_ignored_and_credited() {
23591 let (_pool, mut connection, mut context, mut router, _peer) =
23592 soft_refusal_connection(None, 6);
23593 // Half of this is the threshold at which a stream-0 WINDOW_UPDATE
23594 // credits the received bytes back.
23595 connection.core.connection_config.initial_connection_window = 65_535;
23596 let mut block = vec![0x83, 0x84, 0x87, 0x01, 11];
23597 block.extend_from_slice(b"example.com");
23598 let mut wire = orphan_frame(1, parser::FLAG_END_HEADERS, 5, block.len() as u32, &block);
23599 let window = connection.core.local_settings.settings_initial_window_size as usize;
23600 wire.extend(refused_stream_data(5, window, parser::FLAG_END_STREAM));
23601 let glitches = connection.core.flood_detector.glitch_count();
23602 let frames = frames_after(&mut connection, &mut context, &mut router, &wire);
23603 assert_eq!(
23604 error_code_of(&frames, 3, 5),
23605 Some(H2Error::RefusedStream as u32),
23606 "premise: stream 5 is refused under flood pressure, got {frames:?}"
23607 );
23608 assert_eq!(
23609 frames
23610 .iter()
23611 .filter(|(kind, _, sid, _)| *kind == 3 && *sid == 5)
23612 .count(),
23613 1,
23614 "DATA on a refused stream must not be answered with RST_STREAM, got {frames:?}"
23615 );
23616 assert_eq!(
23617 connection.core.flood_detector.glitch_count(),
23618 glitches + 1,
23619 "DATA within the stream's window on a refused stream costs no glitch"
23620 );
23621 let credited: u32 = frames
23622 .iter()
23623 .filter(|(kind, _, sid, _)| *kind == 8 && *sid == 0)
23624 .map(|(_, _, _, payload)| {
23625 u32::from_be_bytes(payload[..4].try_into().expect("an increment")) & 0x7fff_ffff
23626 })
23627 .sum();
23628 assert_eq!(
23629 credited as usize, window,
23630 "the ignored DATA must be credited to the connection window exactly once"
23631 );
23632 assert_eq!(
23633 error_code_of(&frames, 7, 0),
23634 None,
23635 "no GOAWAY, got {frames:?}"
23636 );
23637 }
23638
23639 /// A drain refuses new streams; a client that sent a whole upload with
23640 /// request trailers on one of them before it read the GOAWAY sees its
23641 /// stream refused, and the drain still ends with GOAWAY(NO_ERROR).
23642 ///
23643 /// TO SEE THIS RED: in `ConnectionH2::refuse_stream_and_discard`, drop
23644 /// the `remember_refused` call. The trailers then end the
23645 /// connection with GOAWAY(PROTOCOL_ERROR) before the drain completes.
23646 #[test]
23647 fn a_drain_with_a_refused_upload_carrying_trailers_ends_with_no_error() {
23648 let (_pool, mut connection, mut context, mut router, _peer) = two_requests_read(usize::MAX);
23649 connection.core.drain.__test_set_draining();
23650 let frames = frames_after(
23651 &mut connection,
23652 &mut context,
23653 &mut router,
23654 &upload_with_trailers(5, 1000),
23655 );
23656 assert_eq!(
23657 error_code_of(&frames, 3, 5),
23658 Some(H2Error::RefusedStream as u32),
23659 "premise: the drain refuses stream 5, got {frames:?}"
23660 );
23661 let now = connection.core.now;
23662 let _ = connection.core.graceful_goaway(now);
23663 let frames = frames_after(&mut connection, &mut context, &mut router, &[]);
23664 assert_eq!(
23665 error_code_of(&frames, 7, 0),
23666 Some(H2Error::NoError as u32),
23667 "the drain must end with GOAWAY(NO_ERROR), got {frames:?}"
23668 );
23669 }
23670
23671 /// A mix of cancels: one stream in ten reset before its response,
23672 /// seventeen in twenty after it, the rest completed. The pre-response
23673 /// resets are the pre-response cap's business; the received-RST lifetime
23674 /// cap compares only the other resets with the answered streams, and must
23675 /// not trip.
23676 ///
23677 /// TO SEE THIS RED: in `H2FloodDetector::record_rst_lifetime`, compare
23678 /// `total_rst_received_lifetime` instead of the resets outside the
23679 /// pre-response count with `streams_opened`. The pre-response resets then
23680 /// push the count past the answered streams and the cap sends GOAWAY.
23681 #[test]
23682 fn pre_response_resets_do_not_count_toward_the_lifetime_cap() {
23683 let (_pool, mut connection, _peer, mut context, mut router) = frontend_rst_fixture(4);
23684 let mut stream_id = 1;
23685 for block in 0..5 {
23686 for slot in 0..20 {
23687 if slot == 19 {
23688 // A stream that completed: counted where the recycle path
23689 // counts it (`try_recycle_server_stream`).
23690 connection.core.flood_detector.record_stream_opened();
23691 continue;
23692 }
23693 let response_started = slot >= 2;
23694 open_routed_frontend_stream(
23695 &mut connection,
23696 &mut context,
23697 stream_id,
23698 response_started,
23699 );
23700 assert!(
23701 !client_cancels(&mut connection, &mut context, &mut router, stream_id),
23702 "block {block}, slot {slot}: a 10 % pre-response / 85 % \
23703 post-response / 5 % clean mix must not send GOAWAY"
23704 );
23705 stream_id += 2;
23706 }
23707 }
23708 }
23709
23710 /// The received-RST lifetime cap still bounds resets of streams that are
23711 /// already gone: here every answered stream is reset twice, so these
23712 /// resets outnumber the answered streams as soon as the floor is passed.
23713 /// Guard against neutering the cap.
23714 #[test]
23715 fn resets_of_streams_already_gone_trip_the_lifetime_cap() {
23716 let (_pool, mut connection, _peer, mut context, mut router) = frontend_rst_fixture(4);
23717 let mut tripped_after = None;
23718 for n in 0..16u32 {
23719 let stream_id = 2 * n + 1;
23720 open_routed_frontend_stream(&mut connection, &mut context, stream_id, true);
23721 if client_cancels(&mut connection, &mut context, &mut router, stream_id)
23722 || client_cancels(&mut connection, &mut context, &mut router, stream_id)
23723 {
23724 tripped_after = Some(n + 1);
23725 break;
23726 }
23727 }
23728 assert_eq!(
23729 tripped_after,
23730 Some(3),
23731 "the reset that takes the count past the floor of 4 while the \
23732 resets outnumber the answered streams must send GOAWAY"
23733 );
23734 }
23735
23736 /// A stream Sōzu answers 502/503/504 without selecting a backend — no
23737 /// backend in the cluster, the session or buffer limit hit before a
23738 /// backend was picked, a routing error — never cost a backend anything,
23739 /// and must not count toward the RST caps' denominator.
23740 ///
23741 /// TO SEE THIS RED: add `|| matches!(stream.context.status,
23742 /// Some(502..=504))` back to `routed_to_a_backend`.
23743 #[test]
23744 fn a_5xx_answered_without_a_backend_is_not_routed() {
23745 let pool = make_pool_for_invariant_16();
23746 let (mut connection, _peer) = test_h2_connection(&pool, None);
23747 let mut context = test_context(&pool);
23748 let answers = context.listener.borrow().get_answers().clone();
23749 for code in [502, 503, 504] {
23750 let gid = context
23751 .create_stream(Ulid::generate(), 65_535)
23752 .expect("the test pool must hand out stream buffers");
23753 crate::protocol::mux::answers::set_default_answer(
23754 &mut context.streams[gid],
23755 &mut connection.core.readiness,
23756 code,
23757 &answers.borrow(),
23758 );
23759 let stream = &context.streams[gid];
23760 assert_eq!(stream.context.status, Some(code), "premise");
23761 assert!(
23762 !routed_to_a_backend(stream),
23763 "a {code} answered before any backend was selected must not count"
23764 );
23765 context.streams[gid].state = StreamState::Recycle;
23766 }
23767 }
23768
23769 /// A stream answered 502/503/504 after a backend was selected for it —
23770 /// the backend refused the connection, timed out, or reset — keeps
23771 /// counting, so a backend outage does not turn the client's ordinary
23772 /// cancels into a Rapid Reset verdict.
23773 ///
23774 /// TO SEE THIS RED: drop `|| stream.context.backend_id.is_some()` from
23775 /// `routed_to_a_backend`.
23776 ///
23777 /// Past the first failures the outage no longer selects a backend at all:
23778 /// every backend is backing off and the 503 is answered with
23779 /// `backends_unavailable` set, which counts the same way.
23780 ///
23781 /// TO SEE THIS RED for the second half: drop
23782 /// `|| stream.context.backends_unavailable` from `routed_to_a_backend`;
23783 /// `test_h2_cancels_during_a_backend_outage_keep_the_connection` then
23784 /// fails end to end too.
23785 #[test]
23786 fn a_5xx_answered_during_a_backend_outage_is_routed() {
23787 let pool = make_pool_for_invariant_16();
23788 let (mut connection, _peer) = test_h2_connection(&pool, None);
23789 let mut context = test_context(&pool);
23790 let answers = context.listener.borrow().get_answers().clone();
23791 for code in [502, 503, 504] {
23792 let gid = context
23793 .create_stream(Ulid::generate(), 65_535)
23794 .expect("the test pool must hand out stream buffers");
23795 context.streams[gid].context.backend_id = Some(Rc::from("down-backend"));
23796 crate::protocol::mux::answers::set_default_answer(
23797 &mut context.streams[gid],
23798 &mut connection.core.readiness,
23799 code,
23800 &answers.borrow(),
23801 );
23802 let stream = &context.streams[gid];
23803 assert_eq!(stream.context.status, Some(code), "premise");
23804 assert!(
23805 routed_to_a_backend(stream),
23806 "a {code} answered for a selected backend must count"
23807 );
23808 context.streams[gid].state = StreamState::Recycle;
23809 }
23810 let gid = context
23811 .create_stream(Ulid::generate(), 65_535)
23812 .expect("the test pool must hand out stream buffers");
23813 context.streams[gid].context.backends_unavailable = true;
23814 crate::protocol::mux::answers::set_default_answer(
23815 &mut context.streams[gid],
23816 &mut connection.core.readiness,
23817 503,
23818 &answers.borrow(),
23819 );
23820 assert!(
23821 routed_to_a_backend(&context.streams[gid]),
23822 "a 503 for a cluster whose backends are all failing must count"
23823 );
23824 }
23825
23826 /// An inbound `GOAWAY` retires every stream above `last_stream_id`
23827 /// without `Connection::end_stream`, so it owes each of their releases.
23828 #[test]
23829 fn invariant_14_an_inbound_goaway_releases_every_retired_charge() {
23830 let pool = make_pool_for_invariant_16();
23831 let mut context = test_context(&pool);
23832 let mut fixture = ledger_fixture(&pool);
23833 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
23834
23835 let _gids = open_streams(&mut fixture, &mut context, 3);
23836 let Connection::H2(shell) = &mut fixture.connection else {
23837 unreachable!("the fixture was built as H2");
23838 };
23839 // `last_stream_id = 0` retires all three: the peer says it processed
23840 // none of them.
23841 shell.core.handle_goaway_frame(
23842 parser::GoAway {
23843 last_stream_id: 0,
23844 error_code: H2Error::NoError as u32,
23845 // Empty debug data: `Slice` is an offset/length pair into
23846 // a buffer this scenario does not carry.
23847 additional_debug_data: kawa::repr::Slice::new(b"", b""),
23848 },
23849 &mut context,
23850 EndpointClient(&mut router),
23851 );
23852
23853 assert_eq!(
23854 reconcile(&context.backend_deltas),
23855 (0, 0),
23856 "every retired stream must release the charge it took"
23857 );
23858 assert_eq!(
23859 settle(&mut fixture, &mut context),
23860 0,
23861 "a GOAWAY that retires every stream must leave no charge behind"
23862 );
23863 }
23864
23865 /// An H1 backend taken back out of the keep-alive pool must be charged
23866 /// for the request it is being reused for.
23867 ///
23868 /// This one records a BEHAVIOUR CHANGE, not just a refactor. Before
23869 /// #1340's Question 12 the charge was taken *before* the inner
23870 /// `start_stream`, under a `BackendStatus::Connected` guard — and an H1
23871 /// connection coming out of the pool is `BackendStatus::KeepAlive` at
23872 /// that moment, because `ConnectionH1::start_stream` is what flips it to
23873 /// `Connected`. So the reuse took no charge, while the matching
23874 /// `Connection::end_stream` released one (it runs before
23875 /// `ConnectionH1::end_stream` flips the status back, so its own
23876 /// `Connected` guard passes). Every keep-alive reuse cycle therefore net
23877 /// `-1`d the backend, `saturating_sub` floored it at zero, and a reused
23878 /// H1 connection reported no in-flight request while it served one —
23879 /// exactly the monotonic drift invariant 14 names, biasing least-loaded
23880 /// and PeakEWMA balancing toward backends that are already busy.
23881 ///
23882 /// Recording the charge after the inner call fixes it, because the flip
23883 /// has happened by then. Taking it afterwards is also what removes the
23884 /// rollback, so the two properties are one edit.
23885 #[test]
23886 fn invariant_14_an_h1_keep_alive_reuse_is_charged() {
23887 let pool = make_pool_for_invariant_16();
23888 let mut context = test_context(&pool);
23889 let (socket, _peer) = connected_socket();
23890 let address: std::net::SocketAddr =
23891 "127.0.0.1:2".parse().expect("backend address must parse");
23892 let backend = Rc::new(RefCell::new(crate::backends::Backend::new(
23893 "h1-keepalive-backend",
23894 address,
23895 None,
23896 None,
23897 None,
23898 )));
23899 let mut registry = crate::protocol::mux::BackendRegistry::default();
23900 let backend_id = registry.id_for(&backend);
23901 let session_ulid = Ulid::generate();
23902 let mut connection = Connection::new_h1_client(
23903 session_ulid,
23904 SessionTcpStream::new(socket, session_ulid, Some(address)),
23905 "h1-keepalive-cluster".into(),
23906 backend_id,
23907 Duration::from_secs(30),
23908 );
23909 // Park it in the pool, the state `Router::plan_connect`'s H1 reuse arm
23910 // picks a backend up from.
23911 if let Position::Client(_, _, status) = connection.position_mut() {
23912 *status = BackendStatus::KeepAlive;
23913 }
23914
23915 let gid = context
23916 .create_stream(Ulid::generate(), 65_535)
23917 .expect("the test pool must hand out stream buffers");
23918 assert!(
23919 connection.start_stream(gid, &mut context),
23920 "premise: a keep-alive H1 backend must accept the reused stream"
23921 );
23922 assert_eq!(
23923 reconcile(&context.backend_deltas),
23924 (1, 0),
23925 "a keep-alive reuse must charge the request it is being reused for"
23926 );
23927
23928 connection.end_stream(gid, &mut context);
23929 assert_eq!(
23930 reconcile(&context.backend_deltas),
23931 (0, 0),
23932 "and the charge must be released when that request ends"
23933 );
23934 let backends = RefCell::new(crate::backends::BackendMap::with_seed(0));
23935 for delta in std::mem::take(&mut context.backend_deltas) {
23936 registry.apply(delta, &backends);
23937 }
23938 assert_eq!(
23939 backend.borrow().active_requests,
23940 0,
23941 "a full reuse cycle must leave active_requests where it found it"
23942 );
23943 }
23944
23945 /// `cancel_timed_out_streams` reaps an idle stream on the same terms:
23946 /// removed outside `Connection::end_stream`, so it releases its own
23947 /// charge.
23948 #[test]
23949 fn invariant_14_a_timed_out_stream_releases_its_charge() {
23950 let pool = make_pool_for_invariant_16();
23951 let mut context = test_context(&pool);
23952 let mut fixture = ledger_fixture(&pool);
23953 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
23954
23955 let _gids = open_streams(&mut fixture, &mut context, 1);
23956 // Push the mux's clock far past the stream idle timeout the fixture
23957 // configured, so the reaper finds the stream expired.
23958 context.now += Duration::from_secs(120);
23959 let Connection::H2(shell) = &mut fixture.connection else {
23960 unreachable!("the fixture was built as H2");
23961 };
23962 let mut endpoint = EndpointClient(&mut router);
23963 shell
23964 .core
23965 .cancel_timed_out_streams(&mut context, &mut endpoint);
23966
23967 assert_eq!(
23968 reconcile(&context.backend_deltas),
23969 (0, 0),
23970 "a reaped stream must release the charge it took"
23971 );
23972 assert_eq!(
23973 settle(&mut fixture, &mut context),
23974 0,
23975 "reaping the only stream must leave no charge behind"
23976 );
23977 }
23978
23979 // ── `backend_header_time`, the H2 half (sozu-proxy/sozu#426) ─────────
23980 //
23981 // DO NOT READ THIS TEST AS COVERING H1. It drives
23982 // `ConnectionH2::handle_headers_frame` only. The H1 twin lives in
23983 // `ConnectionH1::readable` (`lib/src/protocol/mux/h1.rs`) and is pinned by
23984 // its own test there.
23985
23986 /// Stage a response field block in the connection's stream-0 storage and
23987 /// hand back the `Headers` frame that names it, the way
23988 /// `ConnectionH2::handle_read` hands one to
23989 /// `ConnectionH2::handle_headers_frame`: the fragment is a `(start, len)`
23990 /// window over `ConnectionH2::zero`'s buffer, not an owned slice.
23991 ///
23992 /// `0x88` is one HPACK indexed-header-field octet naming static-table
23993 /// entry 8, `:status: 200` (RFC 7541 Appendix A) — a complete, valid
23994 /// response field block in a single byte, so this needs no encoder.
23995 fn stage_response_headers(core: &mut ConnectionH2, stream_id: u32) -> parser::Headers {
23996 core.zero.storage.space()[..1].copy_from_slice(&[0x88]);
23997 core.zero.storage.fill(1);
23998 let buffer = core.zero.storage.buffer();
23999 parser::Headers {
24000 stream_id,
24001 priority: None,
24002 header_block_fragment: kawa::repr::Slice::new(buffer, &buffer[..1]),
24003 end_stream: false,
24004 end_headers: true,
24005 }
24006 }
24007
24008 /// The H2 twin of
24009 /// `the_h1_backend_header_instant_lands_on_the_headers_not_the_response_end`:
24010 /// a backend response HEADERS frame arms the time-to-first-header-byte
24011 /// instant, and it arms it strictly before the response ends.
24012 ///
24013 /// `end_stream: false` is what makes it discriminating — the response body
24014 /// is still outstanding, so `backend_stop` (which every one of
24015 /// `ConnectionH2`'s four sites owns) must still be absent while the header
24016 /// instant already exists. An instant placed at any of those sites cannot
24017 /// satisfy that.
24018 ///
24019 /// The ordering is asserted on `Duration`s rather than on the millisecond
24020 /// values the histogram stores, for the same reason as its H1 twin: at
24021 /// millisecond resolution a fast test rounds both to 0 and the relation
24022 /// holds however wrong the placement is.
24023 ///
24024 /// To SEE THIS RED: move `stream.metrics.backend_headers_received();` out
24025 /// of the `was_initial && self.position.is_client()` branch of
24026 /// `ConnectionH2::handle_headers_frame` and put it immediately after the
24027 /// `stream.metrics.backend_stop();` that opens
24028 /// `ConnectionH2::complete_server_stream` — the nominal `H2::Complete`
24029 /// response-end site. The frame under test carries no END_STREAM, so
24030 /// nothing arms the instant and the test reports no header time at all.
24031 #[test]
24032 fn the_h2_backend_header_instant_lands_on_the_headers_not_the_response_end() {
24033 let pool = make_pool_for_invariant_16();
24034 let mut context = test_context(&pool);
24035 let mut fixture = ledger_fixture(&pool);
24036 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
24037
24038 let gids = open_streams(&mut fixture, &mut context, 1);
24039 let gid = gids[0];
24040 let metrics = &mut context.streams[gid].metrics;
24041 metrics.backend_id = Some("ledger-backend".into());
24042 metrics.backend_start();
24043 metrics.backend_connected();
24044
24045 // Premise: nothing has been received, so neither instant can be stale.
24046 assert_eq!(context.streams[gid].metrics.backend_header_time(), None);
24047 assert_eq!(context.streams[gid].metrics.backend_stop, None);
24048
24049 let Connection::H2(shell) = &mut fixture.connection else {
24050 unreachable!("the fixture was built as H2");
24051 };
24052 let stream_id = *shell
24053 .core
24054 .stream_table
24055 .streams()
24056 .keys()
24057 .next()
24058 .expect("the scenario opened one stream");
24059 assert!(
24060 context.streams[gid].back.is_initial(),
24061 "premise: the backend read side must still be in its initial \
24062 parsing phase, or `was_initial` would not fire"
24063 );
24064
24065 let headers = stage_response_headers(&mut shell.core, stream_id);
24066 shell
24067 .core
24068 .handle_headers_frame(headers, &mut context, EndpointClient(&mut router));
24069
24070 let after_headers = context.streams[gid]
24071 .metrics
24072 .backend_headers_received
24073 .expect("a backend response HEADERS frame must arm the backend header instant");
24074 assert_eq!(
24075 context.streams[gid].metrics.backend_stop, None,
24076 "the frame carried no END_STREAM, so the response-end marker must \
24077 still be absent"
24078 );
24079
24080 // What `ConnectionH2::mark_end_of_stream` does once the response ends.
24081 context.streams[gid].metrics.backend_stop();
24082
24083 let metrics = &context.streams[gid].metrics;
24084 assert_eq!(
24085 metrics.backend_headers_received,
24086 Some(after_headers),
24087 "the response end must not move the header instant"
24088 );
24089 let header_time = metrics
24090 .backend_header_time()
24091 .expect("a completed H2 response must report a backend header time");
24092 let response_time = metrics
24093 .backend_response_time()
24094 .expect("a completed H2 response must report a backend response time");
24095 assert!(
24096 header_time <= response_time,
24097 "the first response-header byte arrives before the last response \
24098 byte: header_time {header_time:?} must not exceed response_time \
24099 {response_time:?}"
24100 );
24101 }
24102
24103 /// The server preface — SETTINGS, the connection-window enlargement and
24104 /// the ACK of the client's SETTINGS — leaves in ONE socket write.
24105 ///
24106 /// On `FrontRustls` every non-empty `socket_write` ends in its own
24107 /// `write_tls`, i.e. its own `writev(2)`. The enlargement used to be
24108 /// queued by `dispatch_writable_state`'s `(ServerSettings, Server)` arm,
24109 /// which runs after `drive_control_flush` already drained `zero`, so it
24110 /// cost a second write pass and a second syscall on every connection.
24111 ///
24112 /// TO SEE THIS RED: move the `gen_window_update` call out of the
24113 /// `(ClientSettings, Server)` readable arm back into a
24114 /// `queue_window_update(0, increment)` in the `(ServerSettings, Server)`
24115 /// writable arm. The write count becomes 2 and the first write carries 66
24116 /// bytes, not 79.
24117 #[test]
24118 fn server_preface_settings_window_update_and_ack_share_one_write() {
24119 use std::io::Read as _;
24120
24121 let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 8, 16384)));
24122 let (mut connection, mut peer) =
24123 connection_with_backpressure(&pool, 0, 0, H2State::ClientPreface);
24124 connection
24125 .core
24126 .readiness
24127 .interest
24128 .insert(Ready::READABLE | Ready::WRITABLE);
24129 let mut context = test_context(&pool);
24130 let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
24131
24132 // Client preface + SETTINGS(MAX_CONCURRENT_STREAMS = 100).
24133 let mut preface = serializer::H2_PRI.as_bytes().to_vec();
24134 preface.extend_from_slice(&[0, 0, 6, 4, 0, 0, 0, 0, 0]);
24135 preface.extend_from_slice(&parser::SETTINGS_MAX_CONCURRENT_STREAMS.to_be_bytes());
24136 preface.extend_from_slice(&100u32.to_be_bytes());
24137 let now = connection.core.now;
24138 liveness_read(
24139 &mut connection,
24140 &mut context,
24141 &mut router,
24142 &mut peer,
24143 now,
24144 &preface,
24145 );
24146 liveness_write(&mut connection, &mut context, &mut router);
24147
24148 assert!(
24149 matches!(connection.core.state, H2State::Header),
24150 "premise: the handshake must have reached the proxying state, got {:?}",
24151 connection.core.state
24152 );
24153 assert_eq!(
24154 connection.socket.writes, 1,
24155 "SETTINGS, WINDOW_UPDATE(0) and the SETTINGS ACK must leave in a \
24156 single socket write"
24157 );
24158
24159 let settings_len = parser::FRAME_HEADER_SIZE
24160 + (parser::SETTINGS_ENTRY_SIZE * parser::SETTINGS_COUNT) as usize;
24161 let expected = settings_len + 13 + serializer::SETTINGS_ACKNOWLEDGEMENT.len();
24162 let mut wire = vec![0u8; expected];
24163 peer.set_read_timeout(Some(Duration::from_secs(5)))
24164 .expect("loopback read timeout must be settable");
24165 peer.read_exact(&mut wire)
24166 .expect("the peer must receive the whole server preface");
24167 assert_eq!(
24168 wire[3], 4,
24169 "the first frame must be SETTINGS (RFC 9113 §3.4)"
24170 );
24171 assert_eq!(wire[4], 0, "the first SETTINGS must not be an ACK");
24172 let window_update = &wire[settings_len..settings_len + 13];
24173 assert_eq!(
24174 window_update[3], 8,
24175 "the second frame must be WINDOW_UPDATE"
24176 );
24177 assert_eq!(&window_update[5..9], &[0, 0, 0, 0], "on the connection");
24178 assert_eq!(
24179 u32::from_be_bytes(window_update[9..13].try_into().unwrap()),
24180 H2ConnectionConfig::default().initial_connection_window - DEFAULT_INITIAL_WINDOW_SIZE,
24181 "the enlargement must grant exactly the configured surplus"
24182 );
24183 assert_eq!(
24184 &wire[settings_len + 13..],
24185 &serializer::SETTINGS_ACKNOWLEDGEMENT,
24186 "the ACK of the client's SETTINGS must close the preface"
24187 );
24188 }
24189
24190 /// A backend that sends SETTINGS again, without granting any connection
24191 /// credit, must not get the one-shot connection-window enlargement again:
24192 /// each repeat would add `initial_connection_window - 65535` to its send
24193 /// window until it passed 2^31-1 (RFC 9113 §6.9.1).
24194 ///
24195 /// TO SEE THIS RED: in `ConnectionH2::handle_settings_frame`, gate the
24196 /// enlargement on `self.flow_control.window() <= DEFAULT_INITIAL_WINDOW_SIZE`
24197 /// (our send window) instead of `connection_window_enlarged`. The three
24198 /// SETTINGS then queue three grants, coalesced into one stream-0 frame
24199 /// of three times the surplus. Verified 2026-10-01 against `3d3f077b`.
24200 #[test]
24201 fn repeated_backend_settings_enlarge_the_connection_window_once() {
24202 let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
24203 let (mut connection, _peer) = test_h2_connection(&pool, None);
24204 let mut context = test_context(&pool);
24205 connection.core.position = Position::Client(
24206 "settings-cluster".into(),
24207 super::super::BackendId {
24208 slot: super::super::BackendSlot(0),
24209 backend_id: Rc::from("settings-backend"),
24210 address: "127.0.0.1:1".parse().expect("a literal socket address"),
24211 metric_incarnation: Default::default(),
24212 },
24213 BackendStatus::Connected,
24214 );
24215 for _ in 0..3 {
24216 let result = connection.core.handle_settings_frame(
24217 parser::Settings {
24218 settings: vec![],
24219 ack: false,
24220 },
24221 &mut context,
24222 );
24223 assert!(
24224 matches!(result, MuxResult::Continue),
24225 "an empty SETTINGS must be accepted"
24226 );
24227 }
24228
24229 let mut buf = vec![0u8; 13 * 8];
24230 let (written, frames) = connection
24231 .core
24232 .flow_control
24233 .drain_window_updates_into(&mut buf);
24234 let grants: Vec<(u32, u32)> = buf[..written]
24235 .as_chunks::<13>()
24236 .0
24237 .iter()
24238 .map(|frame| {
24239 (
24240 u32::from_be_bytes(frame[5..9].try_into().unwrap()),
24241 u32::from_be_bytes(frame[9..13].try_into().unwrap()),
24242 )
24243 })
24244 .collect();
24245 assert_eq!(frames, grants.len());
24246 assert_eq!(
24247 grants,
24248 vec![(
24249 0,
24250 H2ConnectionConfig::default().initial_connection_window
24251 - DEFAULT_INITIAL_WINDOW_SIZE
24252 )],
24253 "three SETTINGS must enlarge the connection window exactly once"
24254 );
24255 }
24256}