Skip to main content

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(&REGISTERED_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}