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            context.stream_bytes_out = context.stream_bytes_out.wrapping_add(committed);
4145            // LIFECYCLE §9 invariant 9, write side. Outbound APPLICATION data
4146            // is activity; an acknowledgement is not. Both halves of this
4147            // condition are load-bearing and neither is implied by reaching
4148            // `H2Shell::write_streams`: an `H2StreamId::Other` id is what
4149            // makes these bytes a stream's rather than `self.zero`'s, and
4150            // `size > 0` is what makes them bytes the socket actually took.
4151            // `arm_timeout` reads `self.now`, the snapshot `writable()` adopted
4152            // at pass entry, so arming from here lands on the same instant the
4153            // top of the pass would have computed — the gate is the whole
4154            // change, not the moment.
4155            if committed > 0 {
4156                self.arm_timeout();
4157            }
4158        }
4159        if resuming {
4160            pass.resume_bytes = pass.resume_bytes.saturating_add(committed);
4161        } else {
4162            pass.stream_bytes = pass.stream_bytes.saturating_add(committed);
4163        }
4164        if let Some(amount) = cross_read_amount {
4165            // Resume path: same stream is parked waiting for buffer space.
4166            // Re-enable READABLE once the write freed enough room.
4167            let kawa = write_buffer(
4168                &mut self.zero,
4169                &mut context.streams,
4170                &self.position,
4171                stream_id,
4172            );
4173            if kawa.storage.available_space() >= amount {
4174                self.readiness.interest.insert(Ready::READABLE);
4175            }
4176        }
4177        pass.stalled = update_readiness_after_write(size, status, &mut self.readiness);
4178    }
4179
4180    /// After forwarding a 1xx informational response (100 Continue, 103 Early
4181    /// Hints), reset the back buffer and re-enable backend readable so the
4182    /// final response can arrive on the same stream. Returns true if the
4183    /// response was 1xx.
4184    fn handle_1xx_reset<E: Endpoint>(
4185        kawa: &mut GenericHttpStream,
4186        stream_state: StreamState,
4187        endpoint: &mut E,
4188    ) -> bool {
4189        let is_1xx = matches!(
4190            kawa.detached.status_line,
4191            kawa::StatusLine::Response { code, .. } if (100..200).contains(&code)
4192        );
4193        if !is_1xx {
4194            return false;
4195        }
4196        debug!(
4197            "{} H2 write_streams: 1xx informational forwarded, resetting back buffer",
4198            log_module_context!()
4199        );
4200        kawa.clear();
4201        if let StreamState::Linked(token) = stream_state {
4202            let readiness = endpoint.readiness_mut(token);
4203            readiness.interest.insert(Ready::READABLE);
4204            readiness.signal_pending_read();
4205        }
4206        true
4207    }
4208
4209    /// Re-arm edge-triggered WRITABLE event if rustls still has buffered TLS data.
4210    ///
4211    /// Takes the answer rather than asking for it: `tls_wants_write` is
4212    /// `H2Shell::tls_wants_write`'s result, read by the caller. That query was
4213    /// this composite's whole remaining socket reach, and handing it in is
4214    /// what leaves a body a byte-in / byte-out core can keep.
4215    ///
4216    /// When the caller reads it is not uniform, and cannot be. Five of the six
4217    /// call sites already hold the answer — four from the `h2_close` decision
4218    /// just taken on it, [`H2Shell::initiate_close_notify`] from its own branch
4219    /// condition — and pass that binding: nothing mutates the socket in
4220    /// between, so a second query would answer the same. The sixth, the
4221    /// [`H2ControlFlushTarget::Stalled`] arm of [`H2Shell::writable`], has no
4222    /// such binding and must read it AFTER the `H2Shell::flush_output_to_socket`
4223    /// whose stall brought it there: that write is what changes the answer.
4224    /// The stalled-drain tails of [`Self::flush_pending_control_frames`] used
4225    /// to read it and re-arm individually; its one output flush now answers
4226    /// [`H2ControlFlushTarget::Stalled`] and that arm re-arms.
4227    ///
4228    /// No re-arm follows a write the kernel refused
4229    /// (`kernel_refused_write`): the socket is full, the kernel raises
4230    /// the next WRITABLE edge once the peer reads, and that edge flushes the
4231    /// records. A synthetic edge would only repeat the refused write, and
4232    /// `Mux::ready_inner` did so on every inner iteration until
4233    /// `MAX_LOOP_ITERATIONS` counted `http.infinite_loop.error` whenever a
4234    /// TLS client stopped reading (sozu-proxy/sozu#1788). The close decisions
4235    /// still read `tls_wants_write` unchanged: a connection with records
4236    /// pending keeps WRITABLE interest and stays open until they are flushed.
4237    pub fn ensure_tls_flushed(&mut self, tls_wants_write: bool) {
4238        if tls_wants_write && !self.kernel_refused_write {
4239            self.readiness.signal_pending_write();
4240        }
4241    }
4242
4243    /// Record what a socket write of this pass answered, for
4244    /// [`Self::ensure_tls_flushed`]. Every write the shell makes reports
4245    /// here, so the flag always describes the latest one.
4246    ///
4247    /// A refused write also consumes the WRITABLE event, as
4248    /// `update_readiness_after_write` does for the writes whose status it
4249    /// reads. The empty-buffer flushes of `H2Shell::flush_tls_records` have
4250    /// no other reader of their status, so without this a refused flush left
4251    /// the edge it was called on in place, and `Mux::ready_inner` ran the
4252    /// pass again on every inner iteration.
4253    fn note_write_status(&mut self, status: SocketResult) {
4254        self.kernel_refused_write = status == SocketResult::WouldBlock;
4255        if self.kernel_refused_write {
4256            self.readiness.event.remove(Ready::WRITABLE);
4257        }
4258    }
4259
4260    /// Evict every per-stream piece of state carried by this `ConnectionH2`.
4261    ///
4262    /// **Invariant**: `stream_table` (wire map, `rst_sent`,
4263    /// `stream_last_activity_at`, `stream_fc_stalled_since`,
4264    /// `stream_fc_stalled_progress`, `expect_read`/`expect_write`) and
4265    /// `prioriser` MUST be emptied of `stream_id` here. `prioriser` is the
4266    /// only per-stream cache not folded into [`h2_stream_table::H2StreamTable`]
4267    /// — this module deliberately does not have the RFC 9218 priority map,
4268    /// same boundary [`h2_flow_control`]'s doc draws for per-stream send
4269    /// window state. `H2StreamTable::remove` asserts its own five caches are
4270    /// clean as a postcondition; forgetting `prioriser` here would still
4271    /// cause unbounded memory growth on long-lived connections with many
4272    /// cancelled streams, so it stays a second, explicit call.
4273    ///
4274    /// When the stream owns the DATA payload being read, the rest of that
4275    /// payload is skipped instead of left unread — see
4276    /// [`Self::skip_orphaned_data_payload`].
4277    fn remove_dead_stream(&mut self, stream_id: StreamId, global_stream_id: GlobalStreamId) {
4278        // Read before `H2StreamTable::remove` nulls `expect_write`: the
4279        // parked DATA dies with its stream (sozu-proxy/sozu#1641).
4280        if matches!(
4281            self.stream_table.expect_write(),
4282            Some(H2StreamId::Other { gid, .. }) if gid == global_stream_id
4283        ) {
4284            let parked_data = std::mem::take(&mut self.parked_data);
4285            if parked_data > 0 {
4286                // An overflow sends GOAWAY(FLOW_CONTROL_ERROR); its outcome
4287                // lives on in the connection state (see the callee's doc).
4288                let _ = self.refund_parked_data(parked_data);
4289            }
4290        }
4291        // Read before `H2StreamTable::remove` nulls `expect_read`.
4292        let orphaned = match (self.stream_table.expect_read(), &self.state) {
4293            (Some((H2StreamId::Other { gid, .. }, remaining)), H2State::Frame(header))
4294                if gid == global_stream_id =>
4295            {
4296                Some((remaining, header.payload_len))
4297            }
4298            _ => None,
4299        };
4300        if self.stream_table.remove(stream_id, global_stream_id)
4301            == h2_stream_table::RemoveOutcome::NotPresent
4302        {
4303            error!(
4304                "{} dead stream_id {} missing from streams map",
4305                log_context!(self),
4306                stream_id
4307            );
4308        }
4309        self.scheduler.remove_stream(&stream_id);
4310        if let Some((remaining, payload_len)) = orphaned {
4311            self.skip_orphaned_data_payload(remaining, payload_len);
4312        }
4313    }
4314
4315    /// Give the connection send window back `dropped` octets of DATA credit
4316    /// spent on frames that were prepared and then dropped before the wire
4317    /// took any of them ([`Self::parked_data`], sozu-proxy/sozu#1641), and
4318    /// re-arm WRITABLE when that reopens the window: every stream starved on
4319    /// it may have released WRITABLE, and nothing else would wake them. The
4320    /// stream's own window is the caller's, since only a live stream has one
4321    /// worth restoring.
4322    ///
4323    /// When the peer's WINDOW_UPDATEs have meanwhile pushed the window so
4324    /// high that the refund would pass 2^31-1, the credit the peer granted
4325    /// overflows (RFC 9113 §6.9.1) and this answers the GOAWAY
4326    /// (FLOW_CONTROL_ERROR) `Self::handle_window_update_frame` sends for an
4327    /// overflowing increment. `Self::goaway` records its outcome in the
4328    /// connection state, so a caller that cannot return it
4329    /// ([`Self::remove_dead_stream`]) may drop the result.
4330    fn refund_parked_data(&mut self, dropped: i32) -> Option<MuxResult> {
4331        debug_assert!(dropped > 0, "only a positive refund is applied");
4332        let window_before = self.flow_control.window();
4333        match self.flow_control.refund_send_window(dropped) {
4334            h2_flow_control::ApplyWindowUpdateOutcome::Applied {
4335                new_window,
4336                should_arm_writable,
4337            } => {
4338                if should_arm_writable {
4339                    self.readiness.arm_writable();
4340                }
4341                debug_assert!(
4342                    new_window > window_before,
4343                    "an applied refund grew the connection window"
4344                );
4345                debug!(
4346                    "{} H2 prepared DATA dropped unsent: {} octets of send credit given back, connection window {}",
4347                    log_context!(self),
4348                    dropped,
4349                    new_window
4350                );
4351                None
4352            }
4353            h2_flow_control::ApplyWindowUpdateOutcome::Overflow => {
4354                debug_assert_eq!(
4355                    self.flow_control.window(),
4356                    window_before,
4357                    "an overflowing refund is not applied"
4358                );
4359                error!(
4360                    "{} INVALID WINDOW INCREMENT: refunding {} octets of dropped DATA passes 2^31-1",
4361                    log_context!(self),
4362                    dropped
4363                );
4364                Some(self.goaway(H2Error::FlowControlError))
4365            }
4366        }
4367    }
4368
4369    /// Empty the encoder's dynamic table after a header block it encoded was
4370    /// dropped unsent, and queue the size updates `0`, then the maximum size,
4371    /// for the next block, which empty the peer's table as well (RFC 7541
4372    /// §4.2; sozu-proxy/sozu#1627). The caller guarantees that no block
4373    /// encoded before this call is still queued for the wire.
4374    fn reset_encoder_table(&mut self) {
4375        let max_size = self.hpack.reset_encoder_table();
4376        let signal = u32::try_from(max_size).unwrap_or(u32::MAX);
4377        debug_assert_eq!(
4378            usize::try_from(signal).ok(),
4379            Some(max_size),
4380            "the encoder's maximum size came from a u32 setting"
4381        );
4382        self.pending_table_size_update = Some(signal);
4383        debug!(
4384            "{} H2 an encoded header block was dropped unsent: HPACK encoder table reset, next block signals 0 then {}",
4385            log_context!(self),
4386            max_size
4387        );
4388        debug_assert!(
4389            self.pending_table_size_update.is_some(),
4390            "the reset is signalled on the next header block"
4391        );
4392    }
4393
4394    /// Keep the connection reading after the stream that owned the DATA
4395    /// payload being read was removed mid-frame (idle reap, reset).
4396    ///
4397    /// Nulling `expect_read` alone left the connection in `H2State::Frame`
4398    /// with nothing to read, so it never read again (#1597). The remaining
4399    /// `remaining` bytes are read and dropped through `H2State::Discard`,
4400    /// in pieces as small as the room in `zero` allows, like HAProxy's
4401    /// "skipping remaining frame payload". `handle_data_frame` will never
4402    /// see this frame, so its whole `payload_len`, padding included, is
4403    /// credited to the connection receive window here (RFC 9113 §6.9), as
4404    /// its closed-stream branch does. READABLE interest is restored because
4405    /// the stream may have been parked on buffer pressure, and
4406    /// `try_resume_reading` only resumes a stream that still exists.
4407    fn skip_orphaned_data_payload(&mut self, remaining: usize, payload_len: u32) {
4408        let conn_threshold = self.connection_config.initial_connection_window / 2;
4409        if let Some(increment) = self
4410            .flow_control
4411            .account_received_bytes(payload_len, conn_threshold)
4412        {
4413            self.queue_window_update(0, increment);
4414            self.readiness.arm_writable();
4415        }
4416        self.state = H2State::Discard;
4417        self.discarded_field_block = None;
4418        self.stream_table
4419            .set_expect_read(Some((H2StreamId::Zero, remaining)));
4420        self.readiness.interest.insert(Ready::READABLE);
4421    }
4422
4423    /// Whether the read in progress is an orphaned DATA remainder that is
4424    /// dropped as it is read ([`Self::skip_orphaned_data_payload`]). A
4425    /// refused HEADERS discard always carries a field block.
4426    fn skipping_orphaned_payload(&self) -> bool {
4427        matches!(self.state, H2State::Discard) && self.discarded_field_block.is_none()
4428    }
4429
4430    /// Drop stream-id mappings for streams that never became active before a
4431    /// connection-level close. This happens on incomplete/oversized header
4432    /// blocks: the stream slot is created on the initial HEADERS frame, then a
4433    /// GOAWAY closes the connection before the request is fully materialized.
4434    pub fn prune_inactive_streams_while_closing<L>(&mut self, context: &mut Context<L>)
4435    where
4436        L: ListenerHandler + L7ListenerHandler,
4437    {
4438        if !self.drain.draining() || !matches!(self.state, H2State::GoAway | H2State::Error) {
4439            return;
4440        }
4441
4442        let stale_streams = self
4443            .stream_table
4444            .streams()
4445            .iter()
4446            .filter_map(|(&stream_id, &global_stream_id)| {
4447                (!context.streams[global_stream_id].state.is_open())
4448                    .then_some((stream_id, global_stream_id))
4449            })
4450            .collect::<Vec<_>>();
4451
4452        for (stream_id, global_stream_id) in stale_streams {
4453            let stream = &mut context.streams[global_stream_id];
4454            if stream.state == StreamState::Idle {
4455                stream.front.clear();
4456                stream.front.storage.clear();
4457                stream.back.clear();
4458                stream.back.storage.clear();
4459                stream.metrics.reset();
4460                stream.state = StreamState::Recycle;
4461            }
4462            self.remove_dead_stream(stream_id, global_stream_id);
4463        }
4464    }
4465
4466    /// Post-write phase: check drain completion, flush TLS, and update readiness.
4467    ///
4468    /// `bytes_written_this_pass` reports the total outbound bytes `write_streams`
4469    /// pushed to the socket (across every stream), and is used to distinguish
4470    /// two very different "no `expect_write`" states:
4471    ///
4472    /// - **Voluntary yield with progress**: at least one DATA/HEADERS frame
4473    ///   emitted, but a stream left non-empty `back.out`/`back.blocks` because
4474    ///   the converter yielded (e.g. RFC 9218 incremental rotation). LIFECYCLE
4475    ///   §9 invariant 16: keep `Ready::WRITABLE` armed so the session loop can
4476    ///   resume flushing on the next tick without waiting for an external
4477    ///   wake-up that edge-triggered epoll will not deliver.
4478    /// - **No progress at all**: converter pushed every block back (e.g. flow
4479    ///   window exhausted, no HEADERS ready yet). Strip `Ready::WRITABLE` —
4480    ///   forward progress must come from an external trigger
4481    ///   (`WINDOW_UPDATE`, new request), not from looping writable().
4482    ///
4483    /// Answers [`H2FinalizeTarget`]: [`H2FinalizeTarget::Done`] in the normal
4484    /// case — `MuxResult::Continue`, or the graceful GOAWAY taken when
4485    /// draining with every stream gone — and otherwise the TLS flush step its
4486    /// caller must perform before [`Self::finalize_write_after_flush`] settles
4487    /// the pass.
4488    ///
4489    /// `tls_wants_write` is `H2Shell::tls_wants_write`'s answer, read by the
4490    /// caller. It is the pre-flush half of the query / flush / query triple
4491    /// this function used to run inline; the flush and the post-flush query
4492    /// are the caller's too, which is what leaves a body a byte-in /
4493    /// byte-out core can keep. The caller reads it before the RFC 9113 §6.8
4494    /// graceful-GOAWAY check below rather than after, which the query's
4495    /// freedom from side effects makes unobservable: on that path the value
4496    /// is discarded.
4497    pub fn finalize_write<L>(
4498        &mut self,
4499        tls_wants_write: bool,
4500        socket_write: bool,
4501        bytes_written_this_pass: usize,
4502        context: &mut Context<L>,
4503    ) -> H2FinalizeTarget
4504    where
4505        L: ListenerHandler + L7ListenerHandler,
4506    {
4507        // RFC 9113 §6.8: if draining and all streams have completed,
4508        // send the final GOAWAY with the actual last_stream_id
4509        if self.drain.draining() && self.stream_table.streams().is_empty() {
4510            return H2FinalizeTarget::Done(self.graceful_goaway(self.now));
4511        }
4512
4513        // The flush triple and the readiness policy behind it are one
4514        // decision, taken in `h2_close` where it is enumerable without a
4515        // socket. Why it is a sibling of `CloseAction` rather than four more
4516        // of its variants, why the conditional middle flush is an INPUT
4517        // instead of an `if` kept here, and why the invariant-16 probe is
4518        // passed as a closure: `h2_close::finalize_action`.
4519        // Bound rather than matched inline: the invariant-16 probe below
4520        // borrows `self.stream_table` and `context.streams`, and a `match`
4521        // keeps its scrutinee's temporaries alive for every arm — including
4522        // the arms that need `&mut self.readiness` and `&mut context.debug`.
4523        let action = h2_close::finalize_action(
4524            TlsFlushPhase::BeforeFlush,
4525            tls_wants_write,
4526            socket_write,
4527            self.stream_table.expect_write().is_some(),
4528            bytes_written_this_pass > 0,
4529            || any_stream_has_pending_back(self.stream_table.streams(), &context.streams),
4530            self.control_tx.has_pending()
4531                || !self.flow_control.pending_window_updates_is_empty()
4532                || !self.output.is_empty(),
4533        );
4534        match action {
4535            // A parked `expect_write` owns the next tick: no bit moves.
4536            FinalizeAction::Parked => H2FinalizeTarget::Done(MuxResult::Continue),
4537            // LIFECYCLE §9 invariant 16: a voluntary scheduler yield left
4538            // stranded bytes in a stream's `back.out`/`back.blocks` after a
4539            // pass that made forward progress. Retaining `Ready::WRITABLE` is
4540            // the ABSENCE of the withdrawal below, so this arm changes no bit
4541            // — it only narrates.
4542            FinalizeAction::RetainPendingBack => {
4543                #[cfg(debug_assertions)]
4544                context.debug.push(DebugEvent::Str(
4545                    "finalize_write: invariant 16 retained WRITABLE (pending back-buffer)"
4546                        .to_owned(),
4547                ));
4548                H2FinalizeTarget::Done(MuxResult::Continue)
4549            }
4550            // Control-frame liveness: a pass that ended before
4551            // `flush_pending_control_frames` drained the RST / WINDOW_UPDATE
4552            // queues, or with bytes still in the ordered output queue, must
4553            // keep `Ready::WRITABLE`. Without this rearm the next tick would
4554            // drop it and the queued frame would stall until an unrelated
4555            // event re-triggered writable — which is exactly the scenario
4556            // h2spec trips by sending back-to-back malformed streams. A
4557            // stalled output flush parks the pass before it gets here, so
4558            // the output leg is defence in depth (#1604).
4559            FinalizeAction::ArmControlQueue => {
4560                #[cfg(debug_assertions)]
4561                context.debug.push(DebugEvent::Str(
4562                    "finalize_write: retained WRITABLE (control queue non-empty)".to_owned(),
4563                ));
4564                self.readiness.arm_writable();
4565                self.metric_events
4566                    .push(MetricEvent::WritableRearmedControlQueue);
4567                H2FinalizeTarget::Done(MuxResult::Continue)
4568            }
4569            // We wrote everything.
4570            FinalizeAction::Quiesce => {
4571                #[cfg(debug_assertions)]
4572                context.debug.push(DebugEvent::Str(format!(
4573                    "Wrote everything: {:?}",
4574                    self.stream_table.streams()
4575                )));
4576                self.readiness.interest.remove(Ready::WRITABLE);
4577                H2FinalizeTarget::Done(MuxResult::Continue)
4578            }
4579            // The two TLS answers differ only in the step the CALLER performs,
4580            // and both come back through the single post-flush query in
4581            // `Self::finalize_write_after_flush`.
4582            FinalizeAction::Flush => H2FinalizeTarget::Flush,
4583            FinalizeAction::SkipFlush => H2FinalizeTarget::SkipFlush,
4584            // Named rather than `other =>`: a wildcard arm would turn a new
4585            // `FinalizeAction` variant into a release-mode panic on the proxy
4586            // write path instead of a compile error.
4587            action @ (FinalizeAction::ReArm | FinalizeAction::Settled) => {
4588                unreachable!("BeforeFlush yielded {action:?}")
4589            }
4590        }
4591    }
4592
4593    /// Settle a write pass whose [`H2FinalizeTarget`] asked for the TLS flush
4594    /// step, given the answer `H2Shell::tls_wants_write` gave AFTER it.
4595    ///
4596    /// The second half of [`Self::finalize_write`]'s protocol, and the reason
4597    /// the split is not a rename: `tls_wants_write` here is a DIFFERENT
4598    /// question from the one that produced [`H2FinalizeTarget::Flush`] or
4599    /// [`H2FinalizeTarget::SkipFlush`]. The first asks whether rustls holds
4600    /// records; this one asks whether the step between them landed —
4601    /// `socket_write(&[])`'s `(size, status)` is discarded at the call site,
4602    /// so nothing else on this path can tell. A caller that reused the first
4603    /// answer here would re-arm forever or never, and either one strands the
4604    /// connection under edge-triggered epoll.
4605    ///
4606    /// The readiness policy of [`Self::finalize_write`] is deliberately NOT
4607    /// reopened: once records were found pending, this pass leaves the bits to
4608    /// the next one, so the degenerate arguments below are the shape
4609    /// `h2_close::goaway_close_action`'s own post-flush call already uses —
4610    /// the inputs the flush could not change are not re-read.
4611    pub fn finalize_write_after_flush(&mut self, tls_wants_write: bool) -> MuxResult {
4612        let action = h2_close::finalize_action(
4613            TlsFlushPhase::AfterFlush,
4614            tls_wants_write,
4615            false,
4616            false,
4617            false,
4618            || false,
4619            false,
4620        );
4621        match action {
4622            // The post-flush answer is read once, by the caller, and feeds
4623            // both the decision and the re-arm: they are one question asked at
4624            // one point, and nothing mutates the socket between them. The
4625            // re-arm still goes through `ensure_tls_flushed` rather than
4626            // touching `signal_pending_write` here, which keeps every
4627            // post-decision TLS re-arm in this file spelled the same way, as
4628            // the GoAway and Error arms in `dispatch_writable_state` and
4629            // `dispatch_writable_state_after_flush` already do.
4630            FinalizeAction::ReArm => self.ensure_tls_flushed(tls_wants_write),
4631            FinalizeAction::Settled => {}
4632            action @ (FinalizeAction::Flush
4633            | FinalizeAction::SkipFlush
4634            | FinalizeAction::Parked
4635            | FinalizeAction::RetainPendingBack
4636            | FinalizeAction::ArmControlQueue
4637            | FinalizeAction::Quiesce) => {
4638                unreachable!("AfterFlush yielded {action:?}")
4639            }
4640        }
4641        MuxResult::Continue
4642    }
4643
4644    /// Queue pending control frames (WINDOW_UPDATEs, RST_STREAMs) behind the
4645    /// ordered output and flush it, before entering the main writable state
4646    /// machine.
4647    ///
4648    /// Answers [`H2ControlFlushTarget`]: [`H2ControlFlushTarget::Proceed`]
4649    /// when `writable()` should carry on into its state dispatch,
4650    /// [`H2ControlFlushTarget::Done`] when a GOAWAY already ended the pass,
4651    /// [`H2ControlFlushTarget::FlushOutput`] when [`Self::output_pending`]
4652    /// holds bytes for the caller to move, and
4653    /// [`H2ControlFlushTarget::Stalled`] when that move stalled — in which
4654    /// case the caller reads `tls_wants_write` and hands it to
4655    /// [`Self::ensure_tls_flushed`].
4656    ///
4657    /// The socket write is the caller's, as everywhere on this write path:
4658    /// the core answers a step, the caller performs the socket work, and the
4659    /// core is told what the socket said, through
4660    /// [`Self::handle_control_flush`].
4661    ///
4662    /// There is one flush, at the end. Every stage appends whole frames to
4663    /// the one ordered output queue (#1604), so a stage no longer has to wait
4664    /// for an earlier one's bytes to leave a shared buffer before it can
4665    /// serialise, and whatever was already queued — an ACK, the rest of a
4666    /// stream frame a partial write cut — stays ahead of what this walk adds.
4667    pub fn flush_pending_control_frames(&mut self) -> H2ControlFlushTarget {
4668        // The peer hung up while the connection drains: nothing queued for it
4669        // will be read. Only OUTPUT is dropped. `zero` is input only, and a
4670        // `Ready::HUP` can be a TCP half-close with frames the peer already
4671        // sent still unread (review of e1c3c2fb, B2), which the shutdown
4672        // drive's forced `readable()` goes on to read.
4673        if self.frontend_hung_up_while_draining() {
4674            self.stream_table.set_expect_write(None);
4675            self.clear_output();
4676            self.flow_control.clear_pending_window_updates();
4677            self.control_tx.clear_pending();
4678        }
4679
4680        // RFC 9113 §6.5: check if peer has timed out on SETTINGS ACK
4681        if let Some(sent_at) = self.settings_sent_at
4682            && self.now.saturating_duration_since(sent_at) >= SETTINGS_ACK_TIMEOUT
4683        {
4684            warn!(
4685                "{} SETTINGS ACK timeout: no SETTINGS ACK observed within {:?}",
4686                log_context!(self),
4687                SETTINGS_ACK_TIMEOUT
4688            );
4689            return H2ControlFlushTarget::Done(self.goaway(H2Error::SettingsTimeout));
4690        }
4691
4692        // Stage — queue pending WINDOW_UPDATE frames.
4693        // Queued inline to avoid extra event loop iterations that could cause
4694        // response data to be sent before subsequent frames are validated.
4695        let pending = self.flow_control.pending_window_updates_len();
4696        if pending > 0 {
4697            let flow_control = &mut self.flow_control;
4698            let mut frames_written = 0;
4699            // Emission order is the deterministic ascending stream_id order
4700            // `H2FlowControl` guarantees — see its module doc — not
4701            // insertion/arrival order.
4702            let _ = self
4703                .output
4704                .push_frames(pending * WINDOW_UPDATE_FRAME_SIZE, |buf| {
4705                    let (offset, frames) = flow_control.drain_window_updates_into(buf);
4706                    frames_written = frames;
4707                    Ok::<_, std::convert::Infallible>(offset)
4708                });
4709            if frames_written > 0 {
4710                self.metric_events
4711                    .push(MetricEvent::WindowUpdateFramesSent(frames_written as i64));
4712            }
4713        }
4714
4715        // Stage — RST_STREAM overflow check + queue.
4716        // The pending queue is bounded; an RST it had to refuse never reaches
4717        // the wire, so the peer would believe that stream still live. Escalate
4718        // before serialising anything. This is a bound on what is pending, not
4719        // a lifetime cap: resets drained as they come never trip it, and the
4720        // peer-provoked ones are capped by the flood detector instead.
4721        if !matches!(self.state, H2State::GoAway | H2State::Error) && self.control_tx.overflowed() {
4722            error!(
4723                "{} pending RST_STREAM queue overflowed its bound {} ({} queued over the connection), sending GOAWAY(ENHANCE_YOUR_CALM)",
4724                log_context!(self),
4725                self.control_tx.max_pending(),
4726                self.control_tx.lifetime_queued(),
4727            );
4728            return H2ControlFlushTarget::Done(self.goaway(H2Error::EnhanceYourCalm));
4729        }
4730
4731        // Queue pending RST_STREAM frames (queued when refusing streams).
4732        // Accounting happens at queue-time inside `Self::enqueue_rst`, so this
4733        // only serialises — no metric/flood calls here would double-count.
4734        // Emission order is queue order, which is arrival order over
4735        // already-deduped stream ids — see `h2_control_tx`'s module doc.
4736        let pending = self.control_tx.pending_len();
4737        if pending > 0 {
4738            let control_tx = &mut self.control_tx;
4739            let _ = self
4740                .output
4741                .push_frames(pending * RST_STREAM_FRAME_SIZE, |buf| {
4742                    let (offset, _frames_written) = control_tx.drain_rst_streams_into(buf);
4743                    Ok::<_, std::convert::Infallible>(offset)
4744                });
4745        }
4746
4747        if self.output.is_empty() {
4748            H2ControlFlushTarget::Proceed
4749        } else {
4750            H2ControlFlushTarget::FlushOutput
4751        }
4752    }
4753
4754    /// Answer the [`H2ControlFlushTarget::FlushOutput`] the caller just
4755    /// performed: `stalled` is what the caller's write reported.
4756    pub fn handle_control_flush(&mut self, stalled: bool) -> H2ControlFlushTarget {
4757        if stalled {
4758            H2ControlFlushTarget::Stalled
4759        } else {
4760            H2ControlFlushTarget::Proceed
4761        }
4762    }
4763
4764    /// Take `writable()`'s `(H2State, Position)` decision, given the answer
4765    /// `H2Shell::tls_wants_write` gave AFTER the pass preamble's flush.
4766    ///
4767    /// `tls_wants_write` is that answer, read by the caller. It is the third
4768    /// step of the preamble's own query / flush / query triple and the
4769    /// pre-flush question both close arms decide on: the
4770    /// `(H2State::Error, Position::Server)` arm has no flush of its own and
4771    /// reads nothing else, while the `H2State::GoAway` arm asks for a second
4772    /// flush through [`H2WritableStateTarget::Flush`]. ONE read serves
4773    /// whichever of the two runs — only one of them can — and nothing mutates
4774    /// the socket between the caller's read and this call. The arms that never
4775    /// consult it discard it, exactly as `H2Shell::write_streams` discards the
4776    /// answer it reads before [`Self::finalize_write`]'s graceful-GOAWAY
4777    /// check; `H2Shell::tls_wants_write` is free of side effects, which is what
4778    /// makes both unobservable.
4779    ///
4780    /// Answers [`H2WritableStateTarget`]. Every arm but two ends the pass with
4781    /// [`H2WritableStateTarget::Done`]; the `H2State::GoAway` arm may ask for
4782    /// the TLS flush step, and the four proxying states name
4783    /// [`H2WritableStateTarget::WriteStreams`]. That last one is the caller's
4784    /// to run for the same reason the flush is — `H2Shell::write_streams` owns
4785    /// the socket and this function does not — and it is why this function
4786    /// takes neither `Context` nor `Endpoint`: no other arm reads either.
4787    pub fn dispatch_writable_state(&mut self, tls_wants_write: bool) -> H2WritableStateTarget {
4788        match (&self.state, &self.position) {
4789            (H2State::Error, Position::Server) => {
4790                // The caller's preamble already attempted this pass's flush,
4791                // so this arm decides on the post-flush answer and has no
4792                // `Flush` of its own — see `h2_close`'s module doc.
4793                match h2_close::error_close_action(tls_wants_write) {
4794                    CloseAction::ReArmAndContinue => {
4795                        self.ensure_tls_flushed(tls_wants_write);
4796                        H2WritableStateTarget::Done(MuxResult::Continue)
4797                    }
4798                    CloseAction::CloseSession => {
4799                        H2WritableStateTarget::Done(MuxResult::CloseSession)
4800                    }
4801                    // Named rather than `other =>`: a wildcard arm would turn a
4802                    // new `CloseAction` variant into a release-mode panic in the
4803                    // proxy write path instead of a compile error.
4804                    action @ (CloseAction::Flush | CloseAction::Disconnect) => {
4805                        unreachable!("error_close_action yielded {action:?}")
4806                    }
4807                }
4808            }
4809            // The preface and SETTINGS are out and the server's SETTINGS are
4810            // not read yet: there is nothing to write until they are. A write
4811            // readiness here is not an error — `start_stream` arms one for a
4812            // stream linked to this connection, which `Router::plan_connect`'s
4813            // pool scan does for a `Connected` connection whatever its handshake
4814            // state — so it is withdrawn, and the SETTINGS ACK the read queues
4815            // re-arms it (sozu-proxy/sozu#1631). Only the control-frame preamble,
4816            // which already ran, may have written: stream frames wait for `Header`.
4817            (H2State::ServerSettings, Position::Client(..)) => {
4818                trace!(
4819                    "{} waiting for the server SETTINGS before writing streams",
4820                    log_context!(self)
4821                );
4822                self.readiness.interest.remove(Ready::WRITABLE);
4823                debug_assert!(
4824                    !self.readiness.interest.is_writable(),
4825                    "a backend awaiting the server SETTINGS must stop asking to write"
4826                );
4827                debug_assert!(
4828                    self.readiness.interest.is_readable(),
4829                    "a backend awaiting the server SETTINGS must keep reading them"
4830                );
4831                H2WritableStateTarget::Done(MuxResult::Continue)
4832            }
4833            (H2State::Error, _) | (H2State::ClientSettings, Position::Server) => {
4834                error!(
4835                    "{} Unexpected combination: (Writable, {:?}, {:?})",
4836                    log_context!(self),
4837                    self.state,
4838                    self.position
4839                );
4840                H2WritableStateTarget::Done(self.force_disconnect())
4841            }
4842            (H2State::ClientPreface, Position::Server) => {
4843                H2WritableStateTarget::Done(MuxResult::Continue)
4844            }
4845            // Discard state: pending data (e.g. RST_STREAM) was already
4846            // written in the caller's control-frame preamble; let the readable
4847            // path consume the remaining frame payload.
4848            (H2State::Discard, _) => H2WritableStateTarget::Done(MuxResult::Continue),
4849            // A stream parked by a stalled write still has whole frames
4850            // queued. The rest of the frame the stall cut, if any, already
4851            // went with the GOAWAY from the ordered output (#1604); finish
4852            // the others before the close decision, since a GOAWAY does not
4853            // close the streams at or below its last stream id (RFC 9113
4854            // §6.8).
4855            (H2State::GoAway, _)
4856                if matches!(
4857                    self.stream_table.expect_write(),
4858                    Some(H2StreamId::Other { .. })
4859                ) =>
4860            {
4861                H2WritableStateTarget::WriteStreams
4862            }
4863            (H2State::GoAway, _) => {
4864                // Response DATA frames may still sit in rustls's output
4865                // buffer — accepted by socket_write_vectored during
4866                // write_streams() but not yet flushed to TCP. Under TCP
4867                // backpressure (HAProxy chain) this is the primary truncation
4868                // vector, so the decision lives in `h2_close`, exhaustively
4869                // unit-tested there rather than inline here. Why the two
4870                // `socket_wants_write()` queries are two DIFFERENT questions,
4871                // and why the flush between them stays inside the same
4872                // `writable()` call: see `h2_close::TlsFlushPhase`. The flush
4873                // and the query after it belong to the caller, and
4874                // `H2WritableStateTarget::Flush` is how this arm asks for them.
4875                match h2_close::goaway_close_action(
4876                    TlsFlushPhase::BeforeFlush,
4877                    self.peer_gone_after_final_goaway(),
4878                    tls_wants_write,
4879                ) {
4880                    CloseAction::CloseSession => {
4881                        H2WritableStateTarget::Done(MuxResult::CloseSession)
4882                    }
4883                    CloseAction::Flush => H2WritableStateTarget::Flush,
4884                    CloseAction::Disconnect => H2WritableStateTarget::Done(self.force_disconnect()),
4885                    action @ CloseAction::ReArmAndContinue => {
4886                        unreachable!("BeforeFlush yielded {action:?}")
4887                    }
4888                }
4889            }
4890            (H2State::ClientPreface, Position::Client(..)) => {
4891                trace!("{} Preparing preface and settings", log_context!(self));
4892                // RFC 9113 §3.4: the preface is the first byte on the wire.
4893                // No stream frame can be queued before it, because no stream
4894                // opens before `Header` (sozu-proxy/sozu#1631).
4895                debug_assert!(
4896                    self.output.is_empty(),
4897                    "nothing may be queued ahead of the client connection preface"
4898                );
4899                self.output.push(serializer::H2_PRI.as_bytes());
4900                let local_settings = &self.local_settings;
4901                match self.output.push_frames(SETTINGS_FRAME_SIZE, |buf| {
4902                    serializer::gen_settings(buf, local_settings).map(|(_, size)| size)
4903                }) {
4904                    Ok(_) => {
4905                        self.metric_events.push(MetricEvent::SettingsFrameSent);
4906                        // RFC 9113 §6.5: start tracking SETTINGS ACK timeout
4907                        self.settings_sent_at = Some(self.now);
4908                    }
4909                    Err(error) => {
4910                        error!(
4911                            "{} Could not serialize SettingsFrame: {:?}",
4912                            log_context!(self),
4913                            error
4914                        );
4915                        return H2WritableStateTarget::Done(self.force_disconnect());
4916                    }
4917                };
4918
4919                self.state = H2State::ClientSettings;
4920                self.readiness.signal_pending_write();
4921                H2WritableStateTarget::Done(MuxResult::Continue)
4922            }
4923            (H2State::ClientSettings, Position::Client(..)) => {
4924                trace!("{} Sent preface and settings", log_context!(self));
4925                self.state = H2State::ServerSettings;
4926                self.stream_table
4927                    .set_expect_read(Some((H2StreamId::Zero, 9)));
4928                // A backend connection starts write-only
4929                // (`Connection::new_h2_client`); it reads from here on, for
4930                // the server's SETTINGS. The preamble's output flush used to
4931                // turn READABLE on as a side effect of draining `zero`.
4932                self.readiness.interest.insert(Ready::READABLE);
4933                self.readiness.interest.remove(Ready::WRITABLE);
4934                H2WritableStateTarget::Done(MuxResult::Continue)
4935            }
4936            (H2State::ServerSettings, Position::Server) => {
4937                // The connection-window enlargement already left with the
4938                // SETTINGS it follows: the `(ClientSettings, Server)` readable
4939                // arm serialises both into `zero`, which the preamble above
4940                // drained in one write.
4941                self.expect_header();
4942                H2WritableStateTarget::Done(MuxResult::Continue)
4943            }
4944            // Proxying states — writing application data (request/response).
4945            // These arms are NOT a filter on what the pass emits: a connection
4946            // whose only queued output is a PING or SETTINGS acknowledgement is
4947            // in `H2State::Header` too, so reaching `write_streams` never meant
4948            // "application data is being written". The frontend timeout is
4949            // therefore armed by `ConnectionH2::handle_write`, per stream
4950            // transmit that moved bytes, and not on the way in here.
4951            (H2State::Header, _)
4952            | (H2State::Frame(_), _)
4953            | (H2State::ContinuationFrame(_), _)
4954            | (H2State::ContinuationHeader(_), _) => H2WritableStateTarget::WriteStreams,
4955        }
4956    }
4957
4958    /// Settle the `H2State::GoAway` arm whose [`H2WritableStateTarget::Flush`]
4959    /// asked for the TLS flush step, given the answer
4960    /// `H2Shell::tls_wants_write` gave AFTER it.
4961    ///
4962    /// The second half of [`Self::dispatch_writable_state`]'s protocol, built
4963    /// the way [`Self::finalize_write_after_flush`] is built and for the same
4964    /// reason: `tls_wants_write` here is a DIFFERENT question from the one
4965    /// that arm decided on. The first asks whether rustls holds records; this
4966    /// one asks whether the flush between them landed —
4967    /// `socket_write(&[])`'s `(size, status)` is discarded at the call site,
4968    /// so nothing else on this path can tell. A caller that reused the first
4969    /// answer here would close a TLS connection with records still pending,
4970    /// and the peer would read a truncated response; that is the truncation
4971    /// vector `h2_close`'s module doc names, not a style point.
4972    ///
4973    /// `peer_gone` is deliberately not re-read — the degenerate `false` below
4974    /// is `h2_close::goaway_close_action`'s own post-flush convention — for
4975    /// the same reason [`Self::finalize_write_after_flush`] does not reopen
4976    /// the readiness policy: the flush cannot resurrect a departed peer, and
4977    /// the decision answers from the one input it could have changed.
4978    pub fn dispatch_writable_state_after_flush(&mut self, tls_wants_write: bool) -> MuxResult {
4979        match h2_close::goaway_close_action(TlsFlushPhase::AfterFlush, false, tls_wants_write) {
4980            CloseAction::ReArmAndContinue => {
4981                self.ensure_tls_flushed(tls_wants_write);
4982                MuxResult::Continue
4983            }
4984            CloseAction::Disconnect => self.force_disconnect(),
4985            // Named rather than `other =>`: a wildcard arm would turn a new
4986            // `CloseAction` variant into a release-mode panic in the proxy
4987            // write path instead of a compile error.
4988            action @ (CloseAction::CloseSession | CloseAction::Flush) => {
4989                unreachable!("AfterFlush yielded {action:?}")
4990            }
4991        }
4992    }
4993
4994    /// Report the access-log RTTs for the local frontend and the linked backend.
4995    ///
4996    /// Both cells are asked of `endpoint`, at the moment the stream is logged
4997    /// and at no other time. `client_rtt` comes from [`Endpoint::local_rtt`]:
4998    /// `ShellEndpoint` samples the frontend socket at the first stream logged
4999    /// on the connection and hands every later stream that one value, so
5000    /// every stream of a connection reports one number (issue #1339 Q11
5001    /// asked for one per pass), and a connection that logs nothing reads
5002    /// nothing (issue #1590). `server_rtt` comes from [`Endpoint::peer_rtt`],
5003    /// the backend connection's own one sample. `doc/configure.md` states
5004    /// both for operators reading the log.
5005    ///
5006    /// Nothing in this method reaches a socket, which is the point: the local
5007    /// RTT was the last `SocketHandler::socket_ref` read in this file, and the
5008    /// shell that owns the socket answers it now.
5009    ///
5010    /// `Position::Server`-only. On a backend H2 connection (`Position::Client`)
5011    /// the report would write swapped values onto the shared `Stream.metrics`:
5012    /// `Mux` samples the frontend into the frontend connection alone, and the
5013    /// corresponding `EndpointServer::peer_rtt` returns that same frontend, so
5014    /// the per-stream `client_rtt`/`server_rtt` cells would be populated with
5015    /// mislabelled values. Gating keeps backend H2 from poisoning the
5016    /// access-log metric for the matching frontend stream.
5017    ///
5018    /// Callers must invoke this BEFORE `endpoint.end_stream(...)` on reset
5019    /// paths so the backend lookup does not depend on
5020    /// `EndpointClient::end_stream` continuing to leave entries in
5021    /// `Router.backends`.
5022    fn snapshot_rtts<E: Endpoint>(
5023        &self,
5024        endpoint: &E,
5025        linked_token: Option<mio::Token>,
5026    ) -> (Option<Duration>, Option<Duration>) {
5027        if !self.position.is_server() {
5028            return (None, None);
5029        }
5030        (
5031            endpoint.local_rtt(),
5032            linked_token.and_then(|t| endpoint.peer_rtt(t)),
5033        )
5034    }
5035
5036    /// Try to recycle a completed server-side stream by distributing overhead,
5037    /// generating access logs, and transitioning the stream to `Recycle` state.
5038    ///
5039    /// Returns `Some((stream_id, Option<token>))` if the stream was recycled, so the
5040    /// caller can add `stream_id` to the dead-streams list and call `endpoint.end_stream()`
5041    /// if a token was returned. Returns `None` if recycling was deferred or not applicable.
5042    ///
5043    /// `client_rtt`/`server_rtt` are snapshotted by the caller — which must
5044    /// do so BEFORE `endpoint.end_stream(...)`, see [`Self::snapshot_rtts`] —
5045    /// and forwarded into the access log. `stream` stays a parameter rather
5046    /// than being looked up here because the caller already holds it as
5047    /// `&mut context.streams[global_stream_id]`.
5048    #[allow(clippy::too_many_arguments)]
5049    fn try_recycle_server_stream<L>(
5050        &mut self,
5051        stream: &mut crate::protocol::mux::Stream,
5052        global_stream_id: GlobalStreamId,
5053        stream_id: StreamId,
5054        byte_totals: (usize, usize),
5055        debug: &mut DebugHistory,
5056        listener: std::rc::Rc<std::cell::RefCell<L>>,
5057        client_rtt: Option<Duration>,
5058        server_rtt: Option<Duration>,
5059    ) -> Option<(StreamId, Option<mio::Token>)>
5060    where
5061        L: ListenerHandler + L7ListenerHandler,
5062    {
5063        match self.position {
5064            Position::Client(..) => None,
5065            Position::Server => {
5066                // Already logged by a reset path; retire the stream after its RST is flushed.
5067                if stream.metrics.start.is_none() {
5068                    let state = std::mem::replace(&mut stream.state, StreamState::Recycle);
5069                    return match state {
5070                        StreamState::Linked(token) => Some((stream_id, Some(token))),
5071                        _ => Some((stream_id, None)),
5072                    };
5073                }
5074
5075                // Don't recycle if the client hasn't sent END_STREAM yet —
5076                // more DATA frames may arrive for this stream.
5077                if !stream.front_received_end_of_stream {
5078                    trace!(
5079                        "{} Defer recycle stream {}: client still sending",
5080                        log_module_context!(),
5081                        global_stream_id
5082                    );
5083                    return None;
5084                }
5085                let stream_bytes = (
5086                    stream.metrics.bin + stream.metrics.backend_bin,
5087                    stream.metrics.bout + stream.metrics.backend_bout,
5088                );
5089                let active_streams = self.stream_table.streams().len();
5090                distribute_overhead(
5091                    &mut stream.metrics,
5092                    &mut self.bytes.overhead_bin,
5093                    &mut self.bytes.overhead_bout,
5094                    stream_bytes,
5095                    byte_totals,
5096                    active_streams,
5097                    active_streams == 1,
5098                );
5099                debug.push(DebugEvent::StreamEvent(4, global_stream_id));
5100                trace!(
5101                    "{} Recycle stream: {}",
5102                    log_module_context!(),
5103                    global_stream_id
5104                );
5105                self.metric_events.push(MetricEvent::EndToEndH2Request);
5106                if routed_to_a_backend(stream) {
5107                    self.flood_detector.record_stream_opened();
5108                }
5109                let (token, events) =
5110                    Self::complete_server_stream(stream, listener, client_rtt, server_rtt);
5111                self.metric_events.extend(events.into_iter().flatten());
5112                Some((stream_id, token))
5113            }
5114        }
5115    }
5116
5117    /// Finalize a server-side stream after its response has been fully written.
5118    ///
5119    /// Generates an access log, resets metrics, and transitions the stream to `Recycle`.
5120    /// Returns the backend token if the stream was `Linked`, so the caller can call
5121    /// `endpoint.end_stream()` with the full `Context` (which can't be passed here
5122    /// because `stream` borrows from `context.streams`).
5123    ///
5124    /// Callers must distribute overhead *before* calling this: it resets
5125    /// `stream.metrics`, so a share credited afterwards would be discarded.
5126    fn complete_server_stream<L>(
5127        stream: &mut crate::protocol::mux::Stream,
5128        listener: std::rc::Rc<std::cell::RefCell<L>>,
5129        client_rtt: Option<Duration>,
5130        server_rtt: Option<Duration>,
5131    ) -> (Option<mio::Token>, [Option<MetricEvent>; 2])
5132    where
5133        L: ListenerHandler + L7ListenerHandler,
5134    {
5135        stream.metrics.backend_stop();
5136        let events = stream.generate_access_log(
5137            false,
5138            Some("H2::Complete"),
5139            listener,
5140            client_rtt,
5141            server_rtt,
5142        );
5143        stream.metrics.reset();
5144        let state = std::mem::replace(&mut stream.state, StreamState::Recycle);
5145        let token = if let StreamState::Linked(token) = state {
5146            Some(token)
5147        } else {
5148            None
5149        };
5150        (token, events)
5151    }
5152
5153    /// Compute the total bytes transferred across all active streams.
5154    ///
5155    /// Returns `(total_bytes_in, total_bytes_out)` where bytes_in = `bin + backend_bin`
5156    /// and bytes_out = `bout + backend_bout` for each stream.
5157    pub fn compute_stream_byte_totals<L: ListenerHandler + L7ListenerHandler>(
5158        &self,
5159        context: &Context<L>,
5160    ) -> (usize, usize) {
5161        let mut total_in = 0usize;
5162        let mut total_out = 0usize;
5163        for &gid in self.stream_table.streams().values() {
5164            let m = &context.streams[gid].metrics;
5165            total_in += m.bin + m.backend_bin;
5166            total_out += m.bout + m.backend_bout;
5167        }
5168        (total_in, total_out)
5169    }
5170
5171    /// Distribute connection-level byte overhead proportionally to a single stream.
5172    ///
5173    /// `totals` should be pre-computed via [`compute_stream_byte_totals`] **before**
5174    /// taking a mutable borrow on the target stream, to avoid borrow conflicts.
5175    /// Delegates to the free function [`distribute_overhead`].
5176    fn distribute_overhead(&mut self, metrics: &mut SessionMetrics, totals: (usize, usize)) {
5177        let stream_bytes = (
5178            metrics.bin + metrics.backend_bin,
5179            metrics.bout + metrics.backend_bout,
5180        );
5181        distribute_overhead(
5182            metrics,
5183            &mut self.bytes.overhead_bin,
5184            &mut self.bytes.overhead_bout,
5185            stream_bytes,
5186            totals,
5187            self.stream_table.streams().len(),
5188            self.stream_table.streams().len() <= 1,
5189        );
5190    }
5191
5192    /// Attribute accumulated `zero_bytes_read` to the stream or to connection overhead.
5193    fn attribute_bytes_to_stream(&mut self, metrics: &mut SessionMetrics) {
5194        self.position
5195            .count_bytes_in(metrics, self.bytes.zero_bytes_read);
5196        self.bytes.zero_bytes_read = 0;
5197    }
5198
5199    fn attribute_bytes_to_overhead(&mut self) {
5200        self.bytes.overhead_bin += self.bytes.zero_bytes_read;
5201        self.bytes.zero_bytes_read = 0;
5202    }
5203
5204    /// Queue a WINDOW_UPDATE, coalescing with any existing entry for the same stream_id.
5205    /// RFC 9113 §6.9.1: window size increment MUST be 1..2^31-1 (0x7FFFFFFF);
5206    /// `increment == 0` is a legal no-op to *send* and queues nothing (see
5207    /// `h2_flow_control::H2FlowControl::queue_window_update`).
5208    ///
5209    /// Always signals pending write so callers don't have to remember the
5210    /// edge-triggered epoll invariant:
5211    /// under ET epoll a queued WINDOW_UPDATE without a live WRITABLE event bit
5212    /// is invisible to filter_interest() and will never get flushed.
5213    fn queue_window_update(&mut self, stream_id: u32, increment: u32) {
5214        match self.flow_control.queue_window_update(
5215            stream_id,
5216            increment,
5217            self.max_pending_window_updates,
5218        ) {
5219            h2_flow_control::QueueWindowUpdateOutcome::Coalesced { old, new } => {
5220                trace!(
5221                    "{} WINDOW_UPDATE coalesced: stream={} old={} new={}",
5222                    log_context!(self),
5223                    stream_id,
5224                    old,
5225                    new
5226                );
5227            }
5228            h2_flow_control::QueueWindowUpdateOutcome::Inserted { increment } => {
5229                trace!(
5230                    "{} WINDOW_UPDATE queued: stream={} increment={}",
5231                    log_context!(self),
5232                    stream_id,
5233                    increment
5234                );
5235            }
5236            h2_flow_control::QueueWindowUpdateOutcome::Dropped => {
5237                error!(
5238                    "{} WINDOW_UPDATE dropped: queue full ({} entries), stream={} increment={}",
5239                    log_context!(self),
5240                    self.max_pending_window_updates,
5241                    stream_id,
5242                    increment
5243                );
5244                self.metric_events.push(MetricEvent::WindowUpdateDropped);
5245            }
5246            // Zero increment: nothing was queued, nothing to log.
5247            h2_flow_control::QueueWindowUpdateOutcome::Noop => {}
5248        }
5249        self.readiness.arm_writable();
5250    }
5251
5252    /// Re-enable READABLE if this connection is parked waiting for buffer space
5253    /// and the target stream's buffer now has enough room.
5254    ///
5255    /// This is the cross-readiness counterpart to the same-connection check in
5256    /// `writable()`. When the *other side* of a stream (frontend or backend)
5257    /// drains data via its own `writable()`, it frees buffer space that this
5258    /// connection was waiting for. Without this explicit wake-up the connection
5259    /// stays parked and the session deadlocks until a timeout fires.
5260    ///
5261    /// Returns `true` if READABLE was re-enabled.
5262    pub fn try_resume_reading<L>(&mut self, context: &Context<L>) -> bool
5263    where
5264        L: ListenerHandler + L7ListenerHandler,
5265    {
5266        if let Some((
5267            H2StreamId::Other {
5268                gid: global_stream_id,
5269                ..
5270            },
5271            amount,
5272        )) = self.stream_table.expect_read()
5273        {
5274            let stream = &context.streams[global_stream_id];
5275            let kawa = match self.position {
5276                Position::Client(..) => &stream.back,
5277                Position::Server => &stream.front,
5278            };
5279            if kawa.storage.available_space() >= amount {
5280                self.readiness.interest.insert(Ready::READABLE);
5281                return true;
5282            }
5283        }
5284        false
5285    }
5286
5287    /// Mark a stream's position-appropriate end-of-stream flag.
5288    ///
5289    /// Server reads from the front (client), so sets `front_received_end_of_stream`.
5290    /// Client reads from the back (backend), so sets `back_received_end_of_stream`.
5291    fn mark_end_of_stream(&self, stream: &mut crate::protocol::mux::Stream) {
5292        if self.position.is_server() {
5293            stream.front_received_end_of_stream = true;
5294        } else {
5295            stream.back_received_end_of_stream = true;
5296        }
5297    }
5298
5299    /// Cancel streams that have been idle longer than [`Self::stream_idle_timeout`].
5300    ///
5301    /// A stream is considered idle when no meaningful application data (non-empty
5302    /// DATA frames or HEADERS) has been received since the last activity timestamp
5303    /// in `H2StreamTable::stream_last_activity_at`.
5304    ///
5305    /// Mitigates slow-multiplex Slowloris (Pass 4 Medium #3): the connection-level
5306    /// idle timer resets on every frame, so a peer sending periodic control frames
5307    /// can pin `max_concurrent_streams` slots for the full nominal connection timeout.
5308    /// Per-stream idle deadlines guarantee each stream terminates if it stops making
5309    /// forward progress, regardless of connection-level liveness.
5310    ///
5311    /// Timed-out streams receive RST_STREAM(CANCEL) and are immediately removed
5312    /// from the streams map so they no longer count against MAX_CONCURRENT_STREAMS.
5313    /// Backend endpoints are notified and metrics are finalized.
5314    pub fn cancel_timed_out_streams<E, L>(&mut self, context: &mut Context<L>, endpoint: &mut E)
5315    where
5316        E: Endpoint,
5317        L: ListenerHandler + L7ListenerHandler,
5318    {
5319        // Entry point: `Mux::timeout` reaches this directly, without going
5320        // through `readable`, so adopt the mux's snapshot here too.
5321        self.now = context.now;
5322        // Per-connection scratch Vecs (`converter_buf`, `lowercase_buf`,
5323        // `cookie_buf`, and the scheduler's own pass-order buffer) grow to a
5324        // high-water mark and never shrink. On a long-lived idle H2
5325        // connection that briefly carried a flurry of large headers, the
5326        // backing memory stays pinned indefinitely. Reclaim past
5327        // `SCRATCH_BUF_RETAIN` when the connection has live streams but
5328        // each scratch buffer holds 4× the cap. Quiet-time only — runs
5329        // at the top of every `cancel_timed_out_streams` invocation
5330        // (which is itself called from the readable hot loop, but only
5331        // on a session that has been idle long enough to risk timing
5332        // out a stream).
5333        const SCRATCH_BUF_RETAIN: usize = 16 * 1024;
5334        self.hpack.reclaim_idle_buffers(SCRATCH_BUF_RETAIN);
5335        self.scheduler.reclaim_idle_buffer(SCRATCH_BUF_RETAIN);
5336
5337        if self.stream_table.is_empty()
5338            || (self.stream_table.activity_is_empty() && self.stream_table.fc_stall_is_empty())
5339        {
5340            return;
5341        }
5342        let now = self.now;
5343        let deadline = self.stream_idle_timeout;
5344        // Two independent per-stream guards reap on the same deadline — see
5345        // `H2StreamTable::collect_timed_out`. The flow-control-stall guard
5346        // (`stream_fc_stalled_since`) closes the HTTP/2 window-stall vector that
5347        // the bidirectional liveness guard (`stream_last_activity_at`) misses,
5348        // because an inbound DATA drip keeps the liveness timer warm while the
5349        // response stays window-blocked.
5350        let timed_out = self.stream_table.collect_timed_out(now, deadline);
5351        if timed_out.is_empty() {
5352            return;
5353        }
5354        for (sid, reason) in timed_out {
5355            info!(
5356                "{} H2 stream {} exceeded {:?} ({}), cancelling",
5357                log_context!(self),
5358                sid,
5359                deadline,
5360                reason
5361            );
5362            // M1: break reaps down by guard so a window-stall reap (a DoS
5363            // mitigation) is distinguishable from an ordinary idle reap on a
5364            // dashboard. M2: a window-stall reap whose stream dribbled some
5365            // outbound progress (`acc > 0`) below the floor is specifically a
5366            // stall-budget reap — the `WINDOW_UPDATE`-drip vector the budget
5367            // closes — counted as a subset. Read the accumulator BEFORE
5368            // `remove_dead_stream` evicts it below.
5369            match reason {
5370                "H2::WindowStall" => {
5371                    self.metric_events
5372                        .push(MetricEvent::StreamReapedWindowStall);
5373                    if matches!(self.stream_table.fc_stall_progress(sid), Some(acc) if acc > 0) {
5374                        self.metric_events
5375                            .push(MetricEvent::StreamReapedStallBudget);
5376                    }
5377                }
5378                "H2::IdleTimeout" => self
5379                    .metric_events
5380                    .push(MetricEvent::StreamReapedIdleTimeout),
5381                other => debug!("{} unexpected reap reason {}", log_context!(self), other),
5382            }
5383            // Route through the canonical chokepoint so dedupe (rst_sent), the
5384            // pending-queue bound, and edge-triggered-epoll arming
5385            // (Readiness::arm_writable) all stay consistent — see LIFECYCLE
5386            // §8.2. The previous direct push bypassed all three: a mass reap
5387            // could push past the queue bound silently, a double-cancel pass
5388            // would grow the pending queue instead of short-circuiting on the
5389            // existing rst_sent membership, and the hand-rolled
5390            // `interest.insert(WRITABLE) + signal_pending_write` pair skipped
5391            // invariant 15.
5392            //
5393            // The CANCEL is Sōzu's own decision (`RstOrigin::Local`), so it is
5394            // not charged to the peer's MadeYouReset count: an idle stream is
5395            // also what a slow backend, a long poll or a paused download looks
5396            // like, and an idle stream pins one MAX_CONCURRENT_STREAMS slot for
5397            // the idle timeout without amplifying anything.
5398            //
5399            // We deliberately ignore the `Option<MuxResult>` signal here —
5400            // `cancel_timed_out_streams` returns `()` and is called as
5401            // best-effort housekeeping during the read path. A local reset
5402            // never trips the flood detector, and a queue overflow is
5403            // escalated by the next `flush_pending_control_frames`.
5404            //
5405            // A backend stream whose HEADERS never left is forgotten, not
5406            // reset: a RST_STREAM on a stream the backend has never seen is a
5407            // connection error that takes down every stream of the shared
5408            // connection (sozu-proxy/sozu#1631, see the `Position::Client` arm
5409            // of `ConnectionH2::end_stream`).
5410            let never_opened = matches!(self.position, Position::Client(..))
5411                && self
5412                    .stream_table
5413                    .get(sid)
5414                    .is_some_and(|gid| !context.streams[gid].front.consumed);
5415            let queued_before = self.control_tx.lifetime_queued();
5416            if !never_opened {
5417                let _ = self.enqueue_rst(sid, H2Error::Cancel, RstOrigin::Local);
5418            }
5419            debug_assert!(
5420                !never_opened || self.control_tx.lifetime_queued() == queued_before,
5421                "a backend stream reaped before its HEADERS left must not be reset"
5422            );
5423
5424            // Remove from streams map and recycle the context stream so the slot
5425            // no longer counts against MAX_CONCURRENT_STREAMS.
5426            // Compute totals per-stream before remove (matches RST_STREAM handler).
5427            let byte_totals = self.compute_stream_byte_totals(context);
5428            if let Some(global_stream_id) = self.stream_table.get(sid) {
5429                {
5430                    let stream = &mut context.streams[global_stream_id];
5431                    self.attribute_bytes_to_stream(&mut stream.metrics);
5432                }
5433                // Check if stream is linked to a backend — borrow must be scoped
5434                // so end_stream can take &mut context.
5435                let linked_token = context.streams[global_stream_id].linked_token();
5436                let (client_rtt, server_rtt) = self.snapshot_rtts(&*endpoint, linked_token);
5437                if let Some(token) = linked_token {
5438                    endpoint.end_stream(token, global_stream_id, context);
5439                }
5440                let stream = &mut context.streams[global_stream_id];
5441                match &self.position {
5442                    Position::Client(_, backend, BackendStatus::Connected) => {
5443                        context.record_backend_delta(backend, BackendChange::StreamsEnded(1));
5444                    }
5445                    Position::Client(..) => {}
5446                    Position::Server => {
5447                        self.distribute_overhead(&mut stream.metrics, byte_totals);
5448                        stream.metrics.backend_stop();
5449                        let events = stream.generate_access_log(
5450                            true,
5451                            Some(reason),
5452                            context.listener.clone(),
5453                            client_rtt,
5454                            server_rtt,
5455                        );
5456                        self.metric_events.extend(events.into_iter().flatten());
5457                        stream.state = StreamState::Recycle;
5458                    }
5459                }
5460                // Retire sid from streams/prioriser/stream_last_activity_at and
5461                // invalidate expect_write/expect_read if they reference this gid.
5462                self.remove_dead_stream(sid, global_stream_id);
5463            }
5464        }
5465        // Writable arming is already done by enqueue_rst -> arm_writable in
5466        // the loop above; the trailing pair was redundant after the chokepoint
5467        // routing landed.
5468    }
5469
5470    /// Queue a `RST_STREAM` frame for serialisation by
5471    /// [`Self::flush_pending_control_frames`] on the next writable tick.
5472    ///
5473    /// This is the canonical entry point for proxy-emitted stream resets:
5474    /// `DATA` on a closed stream, `MAX_CONCURRENT_STREAMS` refusal, and the
5475    /// per-stream error paths in [`Self::reset_stream`] all funnel through
5476    /// here. `origin` says whether the peer provoked the reset; see
5477    /// [`RstOrigin`]. Serialisation is independent of the owning `Stream` still
5478    /// existing in `self.streams`, which is what lets us emit even after a
5479    /// caller has already called [`Self::remove_dead_stream`].
5480    ///
5481    /// Delegates the queueing itself to [`h2_control_tx::H2ControlTx::enqueue_rst`],
5482    /// which owns the invariants (dedupe via `rst_sent`, the per-insert queue
5483    /// bound and its overflow flag, edge-triggered-epoll arm via
5484    /// [`Readiness::arm_writable`]) and covers them with unit tests that need no
5485    /// `ConnectionH2` fixture. What stays here is the accounting, because a
5486    /// MadeYouReset cap trip converts to a connection-wide GOAWAY only this
5487    /// type can return.
5488    ///
5489    /// Two of the call paths that reach here are never inspected by
5490    /// [`Self::check_invariants`], whose only production caller is
5491    /// [`Self::handle_frame`]:
5492    ///
5493    /// * `cancel_timed_out_streams`, which queues one RST per timed-out stream
5494    ///   in a single sweep and, from `Mux::timeout` against a silent peer,
5495    ///   runs when `handle_frame` does not run at all;
5496    /// * the DATA-on-closed-stream reset, which sits in
5497    ///   [`Self::handle_header_state`] — `handle_read` returns into that helper
5498    ///   directly and that branch never reaches `handle_frame`.
5499    ///
5500    /// The second path is separately rate-limited: it is preceded by
5501    /// `H2FloodDetector::record_glitch` + `check_flood_or_return!`, whose
5502    /// `DEFAULT_MAX_GLITCH_COUNT` (2000) bounds it inside one flood window, and
5503    /// the window can only half-decay when `Mux::ready_inner` resamples
5504    /// `context.now` on a new outer iteration — which is strictly after an
5505    /// inner iteration that already ran `writable()`, since `arm_writable`
5506    /// raises both the WRITABLE interest and its event bit. So it cannot
5507    /// accumulate across windows without a `flush_pending_control_frames`
5508    /// pass in between. The per-insert bound inside
5509    /// [`h2_control_tx::H2ControlTx::enqueue_rst`] is what makes that reasoning
5510    /// unnecessary for correctness.
5511    fn enqueue_rst(
5512        &mut self,
5513        wire_stream_id: StreamId,
5514        error: H2Error,
5515        origin: RstOrigin,
5516    ) -> Option<MuxResult> {
5517        // Only a registered stream's id goes into `rst_sent`: eviction from
5518        // the stream table is what removes it again. A refused stream or a
5519        // closed one was never (or is no longer) registered, and its id would
5520        // otherwise stay in the set for the connection's lifetime. A refusal
5521        // records its id in the bounded recently-reset set instead
5522        // (`H2StreamTable::remember_refused`).
5523        let registered = self.stream_table.get(wire_stream_id).is_some();
5524        let rst_sent = registered.then(|| self.stream_table.rst_sent_mut());
5525        let outcome =
5526            self.control_tx
5527                .enqueue_rst(rst_sent, &mut self.readiness, wire_stream_id, error);
5528        // Account ONLY when a new RST actually entered the queue.
5529        // Calling `enqueue_rst` for a stream that already has a queued
5530        // (or already-flushed) RST is the dedup short-circuit — counting
5531        // those would inflate `h2.frames.tx.rst_stream` /
5532        // `h2.rst_stream.sent.*` and the CVE-2025-8671 MadeYouReset
5533        // count on frames that never reached the wire. An
5534        // at-capacity refusal is accounted the same way, and for the same
5535        // reason: that frame never reaches the wire either.
5536        //
5537        // Account at queue-time, not at drain-time. Doing it later in
5538        // `flush_pending_control_frames` would double-count any RST that
5539        // a re-entrant call (DATA on a closed stream we already RSTed)
5540        // tried to enqueue — and missing it at queue-time leaves
5541        // `cancel_timed_out_streams` / `refuse_stream_and_discard` /
5542        // DATA-on-closed-stream paths bypassing the lifetime cap
5543        // (security review LISA-001 on commit `da845c71`).
5544        match outcome {
5545            h2_control_tx::EnqueueRstOutcome::Queued => self.account_emitted_rst(error, origin),
5546            h2_control_tx::EnqueueRstOutcome::Deduped => None,
5547            // Drop + metric + contextual log, never a panic on the release
5548            // path. No GOAWAY is raised here: the refusal set
5549            // `H2ControlTx::overflowed`, which
5550            // `flush_pending_control_frames` escalates to
5551            // `GOAWAY(ENHANCE_YOUR_CALM)` before its drain loop for as long as
5552            // the connection is not already in `H2State::GoAway`/`Error` — and
5553            // once it is, the peer already holds a GOAWAY and `writable()`
5554            // force-disconnects. See `EnqueueRstOutcome::Dropped`. Raising a
5555            // second one from here would re-enter `goaway()` once per
5556            // remaining reaped stream, clobbering `self.zero` and inflating
5557            // `h2.goaway.sent.*`.
5558            h2_control_tx::EnqueueRstOutcome::Dropped => {
5559                error!(
5560                    "{} RST_STREAM dropped: pending queue already at capacity ({}), stream={} error={:?}",
5561                    log_context!(self),
5562                    self.control_tx.max_pending(),
5563                    wire_stream_id,
5564                    error
5565                );
5566                self.metric_events.push(MetricEvent::RstStreamDropped);
5567                None
5568            }
5569        }
5570    }
5571
5572    /// Single accounting site for proxy-emitted RST_STREAM frames.
5573    /// Every emitted RST bumps the global tx counter and the per-error
5574    /// breakdown; only a non-`NoError` reset the peer provoked
5575    /// ([`RstOrigin::PeerProvoked`]) also feeds the MadeYouReset emitted-RST
5576    /// cap.
5577    ///
5578    /// Two distinct emission paths feed this helper:
5579    ///   * Queued frames — `Self::enqueue_rst` (and therefore every
5580    ///     callable that funnels through it: `reset_stream`,
5581    ///     `refuse_stream_and_discard`, `cancel_timed_out_streams`,
5582    ///     DATA-on-closed-stream) calls this once at queue-time. The
5583    ///     drain in `flush_pending_control_frames` does NOT call it
5584    ///     again — that would double-count.
5585    ///   * Converter-emitted frames — the converter's `initialize`
5586    ///     chokepoint (and the HPACK over-budget abort path) writes
5587    ///     RST_STREAM frames straight into `kawa.out` from inside
5588    ///     `kawa.prepare`. We collect those `H2Error` codes during the
5589    ///     `write_streams` loop and call this helper for each one
5590    ///     after the loop. They are all [`RstOrigin::Local`]: what errored
5591    ///     is the message coming from the other side of the proxy — a
5592    ///     backend response on a frontend connection, a client request on a
5593    ///     backend one — never a frame this connection's peer sent.
5594    ///
5595    /// Returning `Some(MuxResult)` means the caller MUST short-circuit
5596    /// with that result — the flood detector tripped its lifetime cap
5597    /// and converted to a connection-wide GOAWAY.
5598    fn account_emitted_rst(&mut self, error: H2Error, origin: RstOrigin) -> Option<MuxResult> {
5599        self.metric_events.push(MetricEvent::RstStreamSent(error));
5600        if origin == RstOrigin::PeerProvoked && !matches!(error, H2Error::NoError) {
5601            if let Some(violation) = self.flood_detector.record_rst_emitted() {
5602                return Some(self.handle_flood_violation(violation));
5603            }
5604            // A provoked reset answers a protocol error by the peer, so it is
5605            // also a glitch: the per-window rate limit the ratio cap above
5606            // lacks. None of the `PeerProvoked` call sites records a glitch
5607            // of its own, so this is the only charge.
5608            self.flood_detector.record_glitch();
5609            if let Some(violation) = self.flood_detector.check_flood(self.now) {
5610                return Some(self.handle_flood_violation(violation));
5611            }
5612        }
5613        None
5614    }
5615
5616    /// Refuse a newly-opened stream with RST_STREAM and discard its HEADERS payload.
5617    ///
5618    /// Used when MAX_CONCURRENT_STREAMS is exceeded or buffer pool is exhausted.
5619    /// Queues the RST_STREAM for the writable path (can't write to kawa.storage
5620    /// here because it is needed to discard the HEADERS payload).
5621    ///
5622    /// Also applies SETTINGS back-pressure per RFC 9113 §5.1.2: if refusals
5623    /// burst past [`BACKPRESSURE_REFUSAL_THRESHOLD`] within
5624    /// [`BACKPRESSURE_WINDOW_DURATION`], the advertised
5625    /// `SETTINGS_MAX_CONCURRENT_STREAMS` is halved via
5626    /// [`Self::apply_mcs_backpressure`].
5627    ///
5628    /// `discarded` is stashed in [`Self::discarded_field_block`] for the
5629    /// `H2State::Discard` arm of [`Self::handle_read`] to consume — see
5630    /// [`DiscardedFieldBlock`] for why the HPACK field block cannot simply be
5631    /// dropped with the rest of the payload.
5632    fn refuse_stream_and_discard(
5633        &mut self,
5634        stream_id: StreamId,
5635        error: H2Error,
5636        origin: RstOrigin,
5637        payload_len: u32,
5638        discarded: DiscardedFieldBlock,
5639    ) -> MuxResult {
5640        if let Some(result) = self.enqueue_rst(stream_id, error, origin) {
5641            return result;
5642        }
5643        // RFC 9113 §5.1: the frames the peer sent on this stream before it
5644        // read the refusal are ignored, as on any stream Sōzu reset.
5645        self.stream_table.remember_refused(stream_id);
5646        let result = self.discard_field_block(stream_id, payload_len, discarded);
5647        self.record_refusal_for_backpressure();
5648        result
5649    }
5650
5651    /// Extend [`Self::flood_refused_streams`] with `stream_id`, refused under
5652    /// flood pressure, or start a new run when a stream was accepted since
5653    /// the last refusal (the even watermark `last_stream_id` moved past the
5654    /// run's first id).
5655    fn note_flood_refused_stream(&mut self, stream_id: StreamId) {
5656        let run = match self.flood_refused_streams {
5657            Some((first, _)) if self.last_stream_id < first => (first, stream_id),
5658            _ => (stream_id, stream_id),
5659        };
5660        debug_assert!(
5661            run.0 <= run.1 && run.1 == stream_id,
5662            "the refused run ends at the stream just refused"
5663        );
5664        debug_assert!(
5665            self.last_stream_id < run.0,
5666            "no accepted stream lies inside the refused run"
5667        );
5668        self.flood_refused_streams = Some(run);
5669    }
5670
5671    /// Read the `payload_len` bytes of a HEADERS or CONTINUATION frame of
5672    /// `stream_id` that is dropped through `H2State::Discard`, which decodes
5673    /// its field block only to keep the HPACK dynamic table in sync (RFC
5674    /// 9113 §4.3), then resumes normal reading. Queues nothing. A frame
5675    /// without END_HEADERS leaves its fragment in
5676    /// [`Self::pending_discarded_block`], so the CONTINUATION frames that
5677    /// complete the block are discarded and decoded with it.
5678    fn discard_field_block(
5679        &mut self,
5680        stream_id: StreamId,
5681        payload_len: u32,
5682        discarded: DiscardedFieldBlock,
5683    ) -> MuxResult {
5684        // A new block's first frame counts toward the block's size like an
5685        // accepted one's (CVE-2024-27316), so its CONTINUATION frames are
5686        // held to the same `max_header_list_size`; the whole frame payload
5687        // stands in for the fragment, which it bounds.
5688        if matches!(discarded, DiscardedFieldBlock::New { .. }) {
5689            self.flood_detector.begin_header_block_if_new(payload_len);
5690        }
5691        self.state = H2State::Discard;
5692        self.stream_table
5693            .set_expect_read(Some((H2StreamId::Zero, payload_len as usize)));
5694        self.discarded_field_block = Some((stream_id, discarded));
5695        MuxResult::Continue
5696    }
5697
5698    /// Minimally process and drop the payload of the DATA or HEADERS frame
5699    /// whose `header` was just parsed, for a stream that is reset or being
5700    /// reset (RFC 9113 §5.1): a field block, with the CONTINUATION frames
5701    /// completing it, still updates the HPACK decoder, DATA still counts
5702    /// toward the connection flow-control window.
5703    fn discard_frame_payload(&mut self, header: &FrameHeader) -> MuxResult {
5704        debug_assert!(
5705            matches!(header.frame_type, FrameType::Data | FrameType::Headers),
5706            "only DATA and HEADERS payloads are discarded per stream"
5707        );
5708        if header.frame_type == FrameType::Headers {
5709            self.discard_field_block(
5710                header.stream_id,
5711                header.payload_len,
5712                DiscardedFieldBlock::New {
5713                    flags: header.flags,
5714                },
5715            )
5716        } else {
5717            self.skip_orphaned_data_payload(header.payload_len as usize, header.payload_len);
5718            MuxResult::Continue
5719        }
5720    }
5721
5722    /// RFC 9113 §5.4.2: answer a stream error on the open stream `header`
5723    /// names with RST_STREAM(`error`), tear the stream down, and discard the
5724    /// frame's payload ([`Self::discard_frame_payload`]). Every other stream
5725    /// on the connection carries on. The RST_STREAM goes through
5726    /// [`Self::reset_stream`], so it feeds the emitted-RST flood accounting.
5727    fn reset_stream_and_discard_frame<E, L>(
5728        &mut self,
5729        header: &FrameHeader,
5730        global_stream_id: GlobalStreamId,
5731        error: H2Error,
5732        context: &mut Context<L>,
5733        endpoint: E,
5734    ) -> MuxResult
5735    where
5736        E: Endpoint,
5737        L: ListenerHandler + L7ListenerHandler,
5738    {
5739        let result =
5740            self.reset_stream(header.stream_id, global_stream_id, context, endpoint, error);
5741        self.remove_dead_stream(header.stream_id, global_stream_id);
5742        if !matches!(result, MuxResult::Continue) {
5743            return result;
5744        }
5745        self.discard_frame_payload(header)
5746    }
5747
5748    /// RFC 9113 §5.1.2 SETTINGS back-pressure bookkeeping.
5749    ///
5750    /// Increments the refusal counter for the current back-pressure window
5751    /// and, when the burst threshold is crossed, halves the advertised
5752    /// `SETTINGS_MAX_CONCURRENT_STREAMS`. Further halving attempts in the
5753    /// same connection are suppressed by [`Self::mcs_backpressure_applied`]
5754    /// so sustained abuse does not collapse the cap to zero — callers can
5755    /// still promote the situation to `EnhanceYourCalm` via the flood
5756    /// detector.
5757    fn record_refusal_for_backpressure(&mut self) {
5758        if self.now.saturating_duration_since(self.refuse_window_start)
5759            >= BACKPRESSURE_WINDOW_DURATION
5760        {
5761            self.refuse_count_window = 0;
5762            self.refuse_window_start = self.now;
5763        }
5764        self.refuse_count_window = self.refuse_count_window.saturating_add(1);
5765        if !self.mcs_backpressure_applied
5766            && self.refuse_count_window >= BACKPRESSURE_REFUSAL_THRESHOLD
5767        {
5768            self.apply_mcs_backpressure();
5769        }
5770    }
5771
5772    /// Halve the advertised `SETTINGS_MAX_CONCURRENT_STREAMS` and mark the
5773    /// back-pressure state as applied. The new value takes effect locally
5774    /// immediately — subsequent stream-open checks in `handle_header_state`
5775    /// compare `self.stream_table.streams().len()` against this reduced cap, so the peer
5776    /// starts receiving `REFUSED_STREAM` earlier. A full SETTINGS re-send on
5777    /// the wire is deferred until we have a mid-connection SETTINGS queue
5778    /// (the existing path in `handle_preface_state` only fires during the
5779    /// handshake); this is noted in the task log as a minimal first step.
5780    fn apply_mcs_backpressure(&mut self) {
5781        let previous = self.local_settings.settings_max_concurrent_streams;
5782        let reduced = (previous / 2).max(1);
5783        warn!(
5784            "{} H2 SETTINGS back-pressure: refusals={} in {}s — halving \
5785             SETTINGS_MAX_CONCURRENT_STREAMS {} -> {}",
5786            log_context!(self),
5787            self.refuse_count_window,
5788            BACKPRESSURE_WINDOW_DURATION.as_secs(),
5789            previous,
5790            reduced,
5791        );
5792        self.local_settings.settings_max_concurrent_streams = reduced;
5793        self.mcs_backpressure_applied = true;
5794    }
5795
5796    /// Log a flood violation with full session context and emit the GOAWAY.
5797    ///
5798    /// Centralises the "flood detected" reporting so every site that observes a
5799    /// `H2FloodViolation` gets the same session-scoped log line, matching the
5800    /// RUSTLS log-context convention. Also emits the per-kind statsd counter
5801    /// (`h2.flood.violation.<kind>`) so SOC dashboards can window the trip
5802    /// rate without parsing logs — every CVE-mitigation in the H2 family
5803    /// (Rapid Reset, MadeYouReset, CONTINUATION/PING/SETTINGS floods, header
5804    /// overflow, glitch) funnels through this site.
5805    pub fn handle_flood_violation(&mut self, violation: H2FloodViolation) -> MuxResult {
5806        self.metric_events.push(MetricEvent::FloodViolation {
5807            metric_key: violation.metric_key,
5808        });
5809        warn!(
5810            "{} H2 flood detected: {} count {} exceeds threshold {}",
5811            log_context!(self),
5812            violation.reason,
5813            violation.count,
5814            violation.threshold,
5815        );
5816        self.goaway(violation.error)
5817    }
5818}
5819
5820/// Whether an answered frontend stream counts toward the RST caps'
5821/// denominator (`H2FloodDetector`'s `streams_opened`,
5822/// `lib/src/protocol/mux/h2_flood_detector.rs`): it was routed to a backend.
5823/// A backend answered it, a backend was selected for it — including when
5824/// Sōzu then answered 502/503/504 because that backend refused the
5825/// connection, failed or timed out — or its cluster has backends and none
5826/// could be selected because all of them are failing (`backends_unavailable`).
5827/// So a backend outage does not turn the client's ordinary cancels into a
5828/// Rapid Reset verdict. A stream answered before any backend was selected
5829/// for another reason does not count, whatever its status: no route, a
5830/// redirect, 401, 421, 429, a refusal, a 503 for a cluster with no backend,
5831/// or a session or buffer limit hit before selection. The peer opens those
5832/// for free, and counting them would let filler streams dilute the ratio.
5833fn routed_to_a_backend(stream: &crate::protocol::mux::Stream) -> bool {
5834    stream.metrics.backend_headers_received.is_some()
5835        || stream.context.backend_id.is_some()
5836        || stream.context.backends_unavailable
5837}
5838
5839/// Who decided a proxy-emitted `RST_STREAM`, for flood accounting.
5840///
5841/// Only a reset the peer provoked counts toward the CVE-2025-8671
5842/// MadeYouReset cap (`H2FloodDetector::record_rst_emitted`,
5843/// `lib/src/protocol/mux/h2_flood_detector.rs`). A reset Sōzu decides on its
5844/// own — the idle reaper's `CANCEL`, a `REFUSED_STREAM` from its own
5845/// concurrency limit, back-pressure, buffer pool or flood soft state, `STREAM_CLOSED` for DATA
5846/// on a stream it already closed, the converter's error on a backend failure —
5847/// says nothing about the peer, and counting it would end healthy connections
5848/// for Sōzu's own decisions.
5849#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5850enum RstOrigin {
5851    /// A frame the peer sent forced the reset: Content-Length mismatch,
5852    /// header parse error, oversized header block, PRIORITY
5853    /// self-dependency, zero-increment or overflowing WINDOW_UPDATE.
5854    PeerProvoked,
5855    /// Sōzu reset the stream on its own initiative.
5856    Local,
5857}
5858
5859/// Recover the [`H2Error`] code that the converter's `initialize`
5860/// chokepoint will encode into the synthesised RST_STREAM frame for a
5861/// kawa stuck in [`kawa::ParsingPhase::Error`]. Mirrors the parse +
5862/// fallback at `lib/src/protocol/mux/converter.rs::initialize` so the
5863/// flood-accounting helper sees the same code that lands on the wire.
5864fn rst_error_from_kawa<T: kawa::AsBuffer>(kawa: &kawa::Kawa<T>) -> H2Error {
5865    match kawa.parsing_phase {
5866        kawa::ParsingPhase::Error {
5867            kind: kawa::ParsingErrorKind::Processing { message },
5868            ..
5869        } => message.parse::<H2Error>().unwrap_or(H2Error::InternalError),
5870        _ => H2Error::InternalError,
5871    }
5872}
5873
5874/// Compile-time mapping from `(prefix, H2Error)` to a static metric key.
5875///
5876/// Materialises a `&'static str` literal via `concat!`, so the metric key
5877/// never crosses through a heap allocation and the statsd drain can store it
5878/// as `&'static str`. Adding a new `H2Error` variant fails the build here —
5879/// the metric breakdown stays in lock-step with RFC 9113 §7 codes.
5880///
5881/// Used for the per-error-code counters emitted around GOAWAY and RST_STREAM
5882/// in either direction (see `metric_for_goaway_sent` etc. below).
5883macro_rules! h2_error_metric_key {
5884    ($prefix:literal, $error:expr) => {
5885        match $error {
5886            H2Error::NoError => concat!($prefix, ".no_error"),
5887            H2Error::ProtocolError => concat!($prefix, ".protocol_error"),
5888            H2Error::InternalError => concat!($prefix, ".internal_error"),
5889            H2Error::FlowControlError => concat!($prefix, ".flow_control_error"),
5890            H2Error::SettingsTimeout => concat!($prefix, ".settings_timeout"),
5891            H2Error::StreamClosed => concat!($prefix, ".stream_closed"),
5892            H2Error::FrameSizeError => concat!($prefix, ".frame_size_error"),
5893            H2Error::RefusedStream => concat!($prefix, ".refused_stream"),
5894            H2Error::Cancel => concat!($prefix, ".cancel"),
5895            H2Error::CompressionError => concat!($prefix, ".compression_error"),
5896            H2Error::ConnectError => concat!($prefix, ".connect_error"),
5897            H2Error::EnhanceYourCalm => concat!($prefix, ".enhance_your_calm"),
5898            H2Error::InadequateSecurity => concat!($prefix, ".inadequate_security"),
5899            H2Error::HTTP11Required => concat!($prefix, ".http_1_1_required"),
5900        }
5901    };
5902}
5903
5904/// Static metric key for an outbound GOAWAY. Same call shape as the other three
5905/// helpers below — keeps the call sites uniform.
5906fn metric_for_goaway_sent(error: H2Error) -> &'static str {
5907    h2_error_metric_key!("h2.goaway.sent", error)
5908}
5909
5910/// Static metric key for an inbound GOAWAY by raw wire error code. Codes
5911/// outside RFC 9113 §7 fall into the dedicated `…unknown_error` bucket so the
5912/// breakdown stays bounded and operators can still spot non-standard peers.
5913fn metric_for_goaway_received(error_code: u32) -> &'static str {
5914    H2Error::try_from(error_code)
5915        .map(|e| h2_error_metric_key!("h2.goaway.received", e))
5916        .unwrap_or("h2.goaway.received.unknown_error")
5917}
5918
5919/// Static metric key for an outbound RST_STREAM. Mirrors
5920/// [`metric_for_goaway_sent`] under a separate namespace so RST and GOAWAY
5921/// rates can be alerted on independently.
5922fn metric_for_rst_stream_sent(error: H2Error) -> &'static str {
5923    h2_error_metric_key!("h2.rst_stream.sent", error)
5924}
5925
5926/// Static metric key for an inbound RST_STREAM by raw wire error code. Same
5927/// `…unknown_error` fallback as [`metric_for_goaway_received`].
5928fn metric_for_rst_stream_received(error_code: u32) -> &'static str {
5929    H2Error::try_from(error_code)
5930        .map(|e| h2_error_metric_key!("h2.rst_stream.received", e))
5931        .unwrap_or("h2.rst_stream.received.unknown_error")
5932}
5933
5934/// Static metric key for an inbound H2 frame by RFC 9113 §6 frame type.
5935/// Emitted at the `handle_frame` dispatch — single chokepoint that any
5936/// new H2 frame type must traverse, so adding a `Frame::*` variant fails
5937/// the build here. Counts are per-frame, not per-byte; pair with
5938/// `bytes_in` for traffic-mix dashboards.
5939fn h2_frame_rx_metric_key(frame_type: FrameType) -> &'static str {
5940    match frame_type {
5941        FrameType::Data => "h2.frames.rx.data",
5942        FrameType::Headers => "h2.frames.rx.headers",
5943        FrameType::PushPromise => "h2.frames.rx.push_promise",
5944        FrameType::Priority => "h2.frames.rx.priority",
5945        FrameType::RstStream => "h2.frames.rx.rst_stream",
5946        FrameType::Settings => "h2.frames.rx.settings",
5947        FrameType::Ping => "h2.frames.rx.ping",
5948        FrameType::GoAway => "h2.frames.rx.goaway",
5949        FrameType::WindowUpdate => "h2.frames.rx.window_update",
5950        FrameType::Continuation => "h2.frames.rx.continuation",
5951        FrameType::PriorityUpdate => "h2.frames.rx.priority_update",
5952        FrameType::Unknown(_) => "h2.frames.rx.unknown",
5953    }
5954}
5955
5956impl ConnectionH2 {
5957    /// Queue the final GOAWAY with `last_stream_id` set to the highest
5958    /// peer-initiated stream observed (`H2StreamTable::highest_peer_stream_id`).
5959    pub fn goaway(&mut self, error: H2Error) -> MuxResult {
5960        let last_stream_id = self.stream_table.highest_peer_stream_id();
5961        self.goaway_with_last_stream_id(error, last_stream_id)
5962    }
5963
5964    /// True while a peer HEADERS block is still being reassembled
5965    /// (`H2State::ContinuationHeader` / `H2State::ContinuationFrame`).
5966    pub fn peer_header_block_in_progress(&self) -> bool {
5967        matches!(
5968            self.state,
5969            H2State::ContinuationHeader(_) | H2State::ContinuationFrame(_)
5970        )
5971    }
5972
5973    /// RFC 9113 §6.8: the final GOAWAY a draining connection sends when its
5974    /// graceful-shutdown budget elapses, just before `Mux::shutting_down_inner`
5975    /// (`lib/src/protocol/mux/mod.rs`) closes it. Without it a forced close
5976    /// leaves the peer holding only the advisory `2^31-1` GOAWAY, which says
5977    /// nothing about which streams were processed (sozu-proxy/sozu#1647).
5978    ///
5979    /// A stream whose opening field block is still being reassembled is
5980    /// excluded: it is `StreamState::Idle`, it never reached a backend, and
5981    /// the peer may retry it. `create_stream` already counted it in
5982    /// `H2StreamTable::highest_peer_stream_id` when its HEADERS frame header
5983    /// arrived. Every stream `create_stream` accepted before it has a lower
5984    /// id (RFC 9113 §5.1.1, enforced by `handle_header_state` against
5985    /// `Self::last_stream_id`), and none can be accepted after it while the
5986    /// block is open (§6.10), so every stream sozu may have acted on is below
5987    /// it: the reported id is the one two below it (the previous odd id, or
5988    /// 0). A higher id counted in `highest_peer_stream_id` can only be a
5989    /// refused stream, which sozu did not act on either.
5990    ///
5991    /// A connection already in `H2State::GoAway` or `H2State::Error` has
5992    /// sent (or failed to send) its final GOAWAY and gets no second one.
5993    pub fn goaway_before_forced_close<L>(&mut self, context: &Context<L>) -> MuxResult
5994    where
5995        L: ListenerHandler + L7ListenerHandler,
5996    {
5997        debug_assert!(
5998            self.drain.draining(),
5999            "a forced close follows a graceful drain, which marks the connection draining"
6000        );
6001        if matches!(self.state, H2State::GoAway | H2State::Error) {
6002            return MuxResult::Continue;
6003        }
6004        let highest = self.stream_table.highest_peer_stream_id();
6005        let last_stream_id = match &self.state {
6006            H2State::ContinuationHeader(headers) | H2State::ContinuationFrame(headers)
6007                if self
6008                    .stream_table
6009                    .get(headers.stream_id)
6010                    .and_then(|global_stream_id| context.streams.get(global_stream_id))
6011                    .is_some_and(|stream| stream.state == StreamState::Idle) =>
6012            {
6013                headers.stream_id.saturating_sub(2)
6014            }
6015            _ => highest,
6016        };
6017        debug_assert!(
6018            last_stream_id <= highest && (last_stream_id == 0 || last_stream_id & 1 == 1),
6019            "the final GOAWAY reports 0 or a peer-initiated id no higher than any observed"
6020        );
6021        self.goaway_with_last_stream_id(H2Error::NoError, last_stream_id)
6022    }
6023
6024    fn goaway_with_last_stream_id(
6025        &mut self,
6026        error: H2Error,
6027        last_stream_id: StreamId,
6028    ) -> MuxResult {
6029        self.state = H2State::Error;
6030        self.drain.enter_final_goaway();
6031        self.graceful_goaway = error == H2Error::NoError;
6032        self.stream_table.set_expect_read(None);
6033        // Disarm the SETTINGS ACK timer: once we've committed to GOAWAY, the
6034        // timeout check at `readable()` / `flush_pending_control_frames()` must
6035        // not re-fire. Without this, `signal_pending_write()` below re-enters
6036        // `writable()` → `flush_pending_control_frames()` on the next tick,
6037        // the elapsed check is still true, and we emit another
6038        // `warn!` + `goaway()` pair, each bumping `h2.goaway.sent.*`.
6039        self.settings_sent_at = None;
6040        // Severity tiering: only `InternalError` implies a sozu-side bug when
6041        // WE emit it. Every other non-`NoError` reason is "peer misbehaved,
6042        // sozu defended correctly" — operators don't need paging on abusive
6043        // or buggy peers. Caller sites already log the specific antecedent
6044        // (flood detected, parser failure, SETTINGS timeout, invalid window)
6045        // before reaching `goaway()`, so demoting this summary line avoids
6046        // duplicate noise without hiding the root cause.
6047        match error {
6048            H2Error::NoError => debug!("{} GOAWAY: {:?}", log_context!(self), error),
6049            H2Error::InternalError => error!("{} GOAWAY: {:?}", log_context!(self), error),
6050            _ => warn!("{} GOAWAY: {:?}", log_context!(self), error),
6051        }
6052        self.metric_events.push(MetricEvent::GoAwaySent(error));
6053
6054        // RFC 9113 §6.8: last_stream_id is the highest peer-initiated stream
6055        // we processed. Queued behind whatever output is already waiting (an
6056        // ACK, the rest of a stream frame), never in place of it.
6057        match self.output.push_frames(GOAWAY_FRAME_SIZE, |buf| {
6058            serializer::gen_goaway(buf, last_stream_id, error).map(|(_, size)| size)
6059        }) {
6060            Ok(_) => {
6061                self.metric_events.push(MetricEvent::GoAwayFrameSent);
6062                self.state = H2State::GoAway;
6063                self.readiness.interest = Ready::WRITABLE | Ready::HUP | Ready::ERROR;
6064                self.readiness.signal_pending_write();
6065                MuxResult::Continue
6066            }
6067            Err(error) => {
6068                error!(
6069                    "{} Could not serialize GoAwayFrame: {:?}",
6070                    log_context!(self),
6071                    error
6072                );
6073                self.force_disconnect()
6074            }
6075        }
6076    }
6077
6078    /// RFC 9113 §6.8: Initiate graceful shutdown using the double-GOAWAY pattern.
6079    ///
6080    /// First call sends GOAWAY with `last_stream_id = 0x7FFFFFFF` (MAX) to signal
6081    /// the intent to stop accepting new streams while allowing in-flight streams
6082    /// to complete. The connection enters draining mode.
6083    ///
6084    /// When `draining` is already true (second invocation), sends the final GOAWAY
6085    /// with the actual `highest_peer_stream_id` so the peer knows which streams
6086    /// were processed.
6087    ///
6088    /// `now` is the caller's clock snapshot and is what arms the forced-close
6089    /// budget. It is a parameter rather than a read of `Self::now` because
6090    /// the caller that matters — `Mux::shutting_down` — runs outside
6091    /// `ready()` and therefore outside the pass that last refreshed the
6092    /// mirror. In-module callers pass `self.now`.
6093    pub fn graceful_goaway(&mut self, now: Instant) -> MuxResult {
6094        // The initial GOAWAY goes out even while a peer HEADERS/CONTINUATION
6095        // block is still being reassembled: RFC 9113 §4.3/§6.10 constrain the
6096        // block's sender, not a GOAWAY travelling the other way, and it is
6097        // queued in `self.output`, which shares nothing with `zero` or
6098        // `header_reassembly`. Deferring it (#1401, removed in #1637) let a
6099        // drain close a session whose only stream was mid-block without any
6100        // GOAWAY — see `h2_drain`'s module doc.
6101        match self.drain.begin_graceful_drain(now) {
6102            // Second GOAWAY: send with the real last_stream_id.
6103            GracefulDrainDecision::AlreadyDraining => self.goaway(H2Error::NoError),
6104            // First GOAWAY: advertise MAX stream ID so the peer knows we are
6105            // draining but does not yet know the cutoff. This gives in-flight
6106            // requests a chance to arrive before we commit to a final
6107            // last_stream_id.
6108            GracefulDrainDecision::SendInitial => self.send_initial_goaway(),
6109        }
6110    }
6111
6112    /// Serializes and queues the first, advisory GOAWAY
6113    /// (`NO_ERROR`, `last_stream_id = STREAM_ID_MAX`) of a graceful drain
6114    /// in the ordered output queue (`self.output`), behind whatever is
6115    /// already queued there.
6116    fn send_initial_goaway(&mut self) -> MuxResult {
6117        // Keep expect_read as-is: existing streams should continue reading
6118        // data during the drain window opened by the initial GOAWAY. Only
6119        // the final GOAWAY (via `goaway()`) removes READABLE.
6120        debug!(
6121            "{} GOAWAY (graceful, initial): last_stream_id=0x7FFFFFFF",
6122            log_context!(self)
6123        );
6124        // The initial GOAWAY sends NO_ERROR on the wire — count it under
6125        // the same per-code key as the final GOAWAY. The downstream alert
6126        // that wants to distinguish drain from termination compares
6127        // against the `h2.goaway.sent.no_error` rate (drain) vs the other
6128        // variants (termination on error).
6129        self.metric_events
6130            .push(MetricEvent::GoAwaySent(H2Error::NoError));
6131
6132        // Queued behind whatever output is already waiting, like the final one.
6133        match self.output.push_frames(GOAWAY_FRAME_SIZE, |buf| {
6134            serializer::gen_goaway(buf, STREAM_ID_MAX, H2Error::NoError).map(|(_, size)| size)
6135        }) {
6136            Ok(_) => {
6137                self.metric_events.push(MetricEvent::GoAwayFrameSent);
6138                // Stay in the current state so the connection can continue processing
6139                // existing streams. The final GOAWAY will transition to GoAway state.
6140                // Keep READABLE so in-flight request bodies can still be received
6141                // during the drain window. Only remove READABLE in the final GOAWAY
6142                // (via `goaway()`).
6143                self.readiness.arm_writable();
6144                MuxResult::Continue
6145            }
6146            Err(error) => {
6147                error!(
6148                    "{} Could not serialize graceful GoAwayFrame: {:?}",
6149                    log_context!(self),
6150                    error
6151                );
6152                self.force_disconnect()
6153            }
6154        }
6155    }
6156
6157    /// Returns `true` when the graceful-shutdown budget armed by
6158    /// [`Self::graceful_goaway`] has elapsed. A return of `true` signals
6159    /// the enclosing session loop that the proxy-initiated drain must
6160    /// transition to a forced close: remaining streams will not complete
6161    /// in time and keeping the connection open past the deadline defeats
6162    /// the soft-stop SLA.
6163    ///
6164    /// Returns `false` when:
6165    /// - drain has not started yet (`started_at` is `None`),
6166    /// - the knob is `0` / `None` (indefinite wait explicitly opted in),
6167    /// - or the elapsed time is still within the configured budget.
6168    pub fn graceful_shutdown_deadline_elapsed(&self) -> bool {
6169        self.drain.deadline_elapsed(self.now)
6170    }
6171
6172    /// Arm the graceful-shutdown budget of a connection a proxy soft-stop
6173    /// finds already draining, which `graceful_goaway` is not called for: a
6174    /// peer GOAWAY drains without arming it. See
6175    /// `H2DrainState::arm_deadline_if_unarmed` (`h2_drain.rs`).
6176    pub fn arm_graceful_shutdown_deadline(&mut self, now: Instant) {
6177        self.drain.arm_deadline_if_unarmed(now);
6178    }
6179
6180    /// True when the reaper has queued control frames (`RST_STREAM`) into
6181    /// `h2_control_tx::H2ControlTx` that have not yet been serialized. Kept SEPARATE
6182    /// from [`H2Shell::has_pending_write`] because that probe gates connection close
6183    /// (the `mod.rs` close-gating sites) and must NOT treat a queued RST as a
6184    /// reason to keep the connection open; this probe is consulted ONLY by the
6185    /// `MuxState::timeout` flush gate to push a silent-peer `RST_STREAM(CANCEL)`
6186    /// onto the wire before the connection closes.
6187    /// Adopt the embedder's clock snapshot for this pass.
6188    ///
6189    /// `Self::now` is mirrored from `Context::now` at each public entry
6190    /// point rather than read from the host — invariant 20 — and a driver
6191    /// standing where `H2Shell::writable` stands has to perform that mirror
6192    /// itself. The other entry points that need it do it inside their own
6193    /// bodies; the write pass is assembled by its caller, so its mirror is
6194    /// exposed.
6195    pub fn adopt_now(&mut self, now: Instant) {
6196        self.now = now;
6197    }
6198
6199    /// The landing zone an [`H2ReadTarget::Fill`] names.
6200    ///
6201    /// The read half of the byte-in / byte-out surface: a driver answers
6202    /// `Fill` by filling this slice and reporting what it managed, and never
6203    /// needs to know which buffer it belongs to. `H2Shell::readable` fills it
6204    /// from a socket; `sim/tests/h2_simulation.rs` fills it from a
6205    /// `VecDeque<u8>`, and that is the whole difference between them.
6206    ///
6207    /// Takes `context` apart from `self` rather than borrowing all of `*self`,
6208    /// for the reason `read_space` gives: the caller must keep the rest of
6209    /// the connection borrowable while the slice is live.
6210    pub fn read_space<'a, L>(
6211        &'a mut self,
6212        context: &'a mut Context<L>,
6213        stream_id: H2StreamId,
6214        amount: usize,
6215    ) -> &'a mut [u8]
6216    where
6217        L: ListenerHandler + L7ListenerHandler,
6218    {
6219        read_space(
6220            &mut self.zero,
6221            &mut context.streams,
6222            &self.position,
6223            stream_id,
6224            amount,
6225        )
6226    }
6227
6228    /// Gather one [`H2WriteTarget::Transmit`]: the ordered output queue
6229    /// first, then `stream_id`'s queued frames, through
6230    /// [`h2_transmit::gather_after`]. Answers `(prefix_len, bytes_offered)`.
6231    ///
6232    /// # Safety
6233    ///
6234    /// [`h2_transmit::gather_after`]'s contract: neither this connection's
6235    /// output queue nor the stream's `kawa` may be mutated or dropped until
6236    /// `io_slices` is emptied, which [`h2_transmit::confirm`] does first.
6237    pub unsafe fn gather_transmit<L>(
6238        &self,
6239        context: &Context<L>,
6240        stream_id: H2StreamId,
6241        io_slices: &mut Vec<std::io::IoSlice<'static>>,
6242    ) -> (usize, usize)
6243    where
6244        L: ListenerHandler + L7ListenerHandler,
6245    {
6246        let H2StreamId::Other { gid, .. } = stream_id else {
6247            unreachable!("no transmit names the connection's input buffer");
6248        };
6249        let stream = &context.streams[gid];
6250        // The WRITE side of `Stream::split`: a server writes the response
6251        // from `back`, a client the request from `front`.
6252        let kawa = match self.position {
6253            Position::Client(..) => &stream.front,
6254            Position::Server => &stream.back,
6255        };
6256        // SAFETY: forwarded, see this function's contract.
6257        unsafe { h2_transmit::gather_after(self.output.pending(), kawa, io_slices) }
6258    }
6259
6260    /// The buffer an [`H2WriteTarget::Transmit`] names.
6261    ///
6262    /// The write half of the same surface, and the one a driver brackets with
6263    /// [`Self::gather_transmit`] and [`h2_transmit::confirm`] around its own
6264    /// vectored write. See `write_buffer` for why it takes `context` apart
6265    /// from `self`.
6266    pub fn write_buffer<'a, L>(
6267        &'a mut self,
6268        context: &'a mut Context<L>,
6269        stream_id: H2StreamId,
6270    ) -> &'a mut GenericHttpStream
6271    where
6272        L: ListenerHandler + L7ListenerHandler,
6273    {
6274        write_buffer(
6275            &mut self.zero,
6276            &mut context.streams,
6277            &self.position,
6278            stream_id,
6279        )
6280    }
6281
6282    /// The queued output bytes an [`H2ControlFlushTarget::FlushOutput`] is
6283    /// asking the caller to move, or empty when there are none left.
6284    pub fn output_pending(&self) -> &[u8] {
6285        self.output.pending()
6286    }
6287
6288    /// Whether [`Self::output_pending`] ends with the last bytes this
6289    /// connection will ever send, so the caller may queue the TLS
6290    /// `close_notify` behind them and flush both at once.
6291    ///
6292    /// Only [`Self::goaway`] sets [`H2State::GoAway`], and only once its final
6293    /// GOAWAY is queued; the state's writable arm then closes
6294    /// (`h2_close::goaway_close_action`). Every other condition rules out a
6295    /// write the same pass would still make after this flush:
6296    ///
6297    /// - no stream left: no stream frame follows, and no stream is parked
6298    ///   (`H2StreamTable::remove` nulls its park);
6299    /// - no WINDOW_UPDATE or RST_STREAM queued: the preamble already moved
6300    ///   every one of them into the output this flush drains;
6301    /// - `Position::Server`: `close_notify` is a frontend TLS alert, as in
6302    ///   [`H2Shell::initiate_close_notify`];
6303    /// - `close_notify_sent` unset: the alert is queued once.
6304    fn output_flush_closes_connection(&self) -> bool {
6305        self.position.is_server()
6306            && matches!(self.state, H2State::GoAway)
6307            && !self.close_notify_sent
6308            && self.stream_table.streams().is_empty()
6309            && self.flow_control.pending_window_updates_is_empty()
6310            && !self.control_tx.has_pending()
6311    }
6312
6313    /// Account for one write of [`Self::output_pending`], and answer whether
6314    /// the caller should stop.
6315    ///
6316    /// `true` means the write stalled (WouldBlock or a zero-length write) and
6317    /// the caller must wait for its next writable event; `false` means carry
6318    /// on until [`Self::output_pending`] is empty.
6319    pub fn consume_output_flush(&mut self, size: usize, status: SocketResult) -> bool {
6320        self.note_write_status(status);
6321        self.consume_output(size);
6322        update_readiness_after_write(size, status, &mut self.readiness)
6323    }
6324
6325    /// [`H2Shell::has_pending_write`] with the TLS answer supplied instead of
6326    /// queried — the byte-in / byte-out spelling of the same probe.
6327    ///
6328    /// A driver with no TLS layer passes `false` and gets the connection's own
6329    /// two conditions back.
6330    pub fn has_pending_write_with(&self, tls_wants_write: bool) -> bool {
6331        if self.peer_gone_after_final_goaway() {
6332            return false;
6333        }
6334        self.stream_table.expect_write().is_some() || !self.output.is_empty() || tls_wants_write
6335    }
6336
6337    pub fn has_pending_control_write(&self) -> bool {
6338        self.control_tx.has_pending()
6339    }
6340
6341    /// Number of streams currently tracked in the wire map. `stream_table` is
6342    /// private to this module, so this is the sole accessor for `router.rs`'s
6343    /// H2-connection-reuse heuristic (picks the non-draining H2 backend
6344    /// connection with the fewest active streams).
6345    pub fn stream_count(&self) -> usize {
6346        self.stream_table.len()
6347    }
6348
6349    pub fn create_stream<L>(
6350        &mut self,
6351        stream_id: StreamId,
6352        context: &mut Context<L>,
6353    ) -> Option<GlobalStreamId>
6354    where
6355        L: ListenerHandler + L7ListenerHandler,
6356    {
6357        // RFC 9113 §6.8: reject new streams on a draining connection
6358        if self.drain.draining() {
6359            error!(
6360                "{} Rejecting new stream {} on draining connection",
6361                log_context!(self),
6362                stream_id
6363            );
6364            return None;
6365        }
6366        let streams_before = self.stream_table.len();
6367        // Track the highest peer-initiated stream ID for GoAway frames
6368        // before any early return, so GoAway always reports the correct last
6369        // stream. `observe_peer_stream_id` asserts the monotonic-non-decreasing
6370        // property itself.
6371        self.stream_table.observe_peer_stream_id(stream_id);
6372        // `Ulid::generate()` would read the wall clock and a thread-local RNG
6373        // from inside `rusty_ulid` — two host reaches no grep over this module
6374        // can see. `Context::next_request_id` composes the same ULID from the
6375        // two sources the embedder owns instead.
6376        let request_id = context.next_request_id();
6377        let global_stream_id =
6378            context.create_stream(request_id, self.peer_settings.settings_initial_window_size)?;
6379        self.last_stream_id = (stream_id + 2) & !1;
6380        self.stream_table
6381            .register(stream_id, global_stream_id, self.now);
6382        // Post-conditions: the stream is now reachable in both indices (see
6383        // `H2StreamTable::register`'s own postconditions), the active count
6384        // grew by exactly one (the id was not already present —
6385        // `handle_header_state` rejects re-used ids), and `last_stream_id` is
6386        // the even watermark just past this id so `new_stream_id` never collides.
6387        debug_assert_eq!(
6388            self.stream_table.len(),
6389            streams_before + 1,
6390            "create_stream must add exactly one stream (id must not pre-exist)"
6391        );
6392        debug_assert!(
6393            self.last_stream_id > stream_id && self.last_stream_id & 1 == 0,
6394            "last_stream_id watermark must be the even value strictly above stream_id"
6395        );
6396        Some(global_stream_id)
6397    }
6398
6399    pub fn new_stream_id(&mut self) -> Option<StreamId> {
6400        let watermark_before = self.last_stream_id;
6401        let (issued, next) = next_stream_id(self.last_stream_id, self.position.is_client())?;
6402        self.last_stream_id = next;
6403        // Post-conditions: the locally-issued id has the parity of our role and
6404        // the watermark advanced strictly (so the next allocation cannot reuse
6405        // this id). `next_stream_id` already asserts parity vs `is_client`; here
6406        // we re-assert against `self.position` and the watermark monotonicity.
6407        debug_assert_eq!(
6408            issued & 1 == 1,
6409            self.position.is_client(),
6410            "locally-issued stream id parity must match our role"
6411        );
6412        debug_assert!(
6413            self.last_stream_id > watermark_before,
6414            "issuing a stream id must advance the watermark"
6415        );
6416        Some(issued)
6417    }
6418
6419    /// Test-only setter: jump `last_stream_id` close to `STREAM_ID_MAX` so
6420    /// that the next call to [`Self::new_stream_id`] exhausts the 31-bit
6421    /// space. FIX-22 ("Stream-ID exhaustion disconnects backend gracefully")
6422    /// exercises the `None`-return branch — reaching it through normal API
6423    /// usage would require issuing ~2³¹ requests, which is not tractable in
6424    /// an E2E harness.
6425    #[cfg(any(test, feature = "e2e-hooks"))]
6426    pub fn __test_set_last_stream_id(&mut self, id: StreamId) {
6427        self.last_stream_id = id;
6428    }
6429
6430    /// Test-only setter: map `stream_id -> gid` on the wire WITHOUT arming
6431    /// the per-stream liveness timer `stream_table.register` otherwise always
6432    /// pairs it with. `mod::tests::shutting_down_refreshes_the_snapshot_so_the_drain_budget_expires`
6433    /// needs exactly this artificial state, to force `cancel_timed_out_streams`
6434    /// down its early-return path (empty activity map) rather than race an
6435    /// RST_STREAM into that test's unrelated drain-budget assertion.
6436    #[cfg(any(test, feature = "e2e-hooks"))]
6437    pub fn __test_insert_wire_mapping_only(&mut self, stream_id: StreamId, gid: GlobalStreamId) {
6438        self.stream_table
6439            .__test_insert_wire_mapping_only(stream_id, gid);
6440    }
6441
6442    /// Cross-field invariant sweep for the H2 connection state machine,
6443    /// asserted as a run-to-completion post-condition at the end of every
6444    /// frame-handling pass (see the call in [`Self::handle_frame`]).
6445    ///
6446    /// These are relationships between *separate* fields that no single setter
6447    /// can guarantee on its own — exactly the class of bug TigerStyle's
6448    /// `check_invariants` targets. Each one is cheap (counter compares + a few
6449    /// `HashMap` membership probes); the whole function is `#[cfg(debug_assertions)]`
6450    /// and compiles out of release entirely.
6451    ///
6452    /// Encoded invariants:
6453    /// 1. **Stream-id watermark parity**: locally-issued ids never exceed
6454    ///    `STREAM_ID_MAX`; `last_stream_id` stays the even watermark (it is
6455    ///    rounded to `(id + 2) & !1` and initialised to 0).
6456    /// 2. **Per-stream caches are subsets of the live stream set**:
6457    ///    `stream_last_activity_at` is keyed only by currently-tracked stream
6458    ///    ids — a leak here would let a removed stream keep an idle timer and
6459    ///    mis-fire `cancel_timed_out_streams`. (`rst_sent` is intentionally NOT
6460    ///    a subset: a queued RST for an already-removed stream is legal.)
6461    /// 3. **RST queue accounting** is checked by
6462    ///    [`h2_control_tx::H2ControlTx::check_invariants`] as a post-condition
6463    ///    of its own mutating methods, so it is not restated here — a second
6464    ///    copy of a rule drifts from the first.
6465    /// 4. **Pending WINDOW_UPDATE bound**: the coalescing map never exceeds the
6466    ///    per-connection cap derived from `max_concurrent_streams`.
6467    /// 5. **Drain/state coupling**: a terminal `GoAway`/`Error` state implies the
6468    ///    connection is draining (`goaway()` sets both); the converse need not
6469    ///    hold (graceful drain stays in a live state).
6470    #[cfg(debug_assertions)]
6471    fn check_invariants<L>(&self, context: &Context<L>)
6472    where
6473        L: ListenerHandler + L7ListenerHandler,
6474    {
6475        // (1) Watermark parity and bound.
6476        debug_assert!(
6477            self.last_stream_id & 1 == 0,
6478            "last_stream_id must stay an even watermark, got {}",
6479            self.last_stream_id
6480        );
6481
6482        // (2) Per-stream caches are subsets of the live stream set, and every
6483        // mapping points at a valid context slot.
6484        debug_assert!(
6485            self.stream_table
6486                .stream_last_activity_at()
6487                .keys()
6488                .all(|id| self.stream_table.streams().contains_key(id)),
6489            "stream_last_activity_at must only track currently-open stream ids"
6490        );
6491        debug_assert!(
6492            self.stream_table
6493                .streams()
6494                .values()
6495                .all(|&gid| gid < context.streams.len()),
6496            "every stream mapping must point at a valid context slot"
6497        );
6498
6499        // (4) Pending WINDOW_UPDATE coalescing map bound.
6500        debug_assert!(
6501            self.flow_control.pending_window_updates_len() <= self.max_pending_window_updates,
6502            "pending WINDOW_UPDATE map must stay within its per-connection cap"
6503        );
6504
6505        // (5) Drain/state coupling: terminal states imply draining.
6506        debug_assert!(
6507            !matches!(self.state, H2State::GoAway | H2State::Error) || self.drain.draining(),
6508            "GoAway/Error state must imply the connection is draining"
6509        );
6510    }
6511
6512    fn handle_frame<E, L>(
6513        &mut self,
6514        frame: Frame,
6515        wire_payload_len: u32,
6516        context: &mut Context<L>,
6517        endpoint: E,
6518    ) -> MuxResult
6519    where
6520        E: Endpoint,
6521        L: ListenerHandler + L7ListenerHandler,
6522    {
6523        trace!("{} {:#?}", log_context!(self), frame);
6524        // Per-frame-type RX counter. Single chokepoint covers every H2 frame
6525        // type — adding a new `Frame::*` variant fails the build inside the
6526        // helper, keeping the metric breakdown in lock-step with RFC 9113 §6.
6527        self.metric_events
6528            .push(MetricEvent::FrameReceived(frame.frame_type()));
6529        let result = match frame {
6530            Frame::Data(data) => self.handle_data_frame(data, wire_payload_len, context, endpoint),
6531            Frame::Headers(headers) => self.handle_headers_frame(headers, context, endpoint),
6532            Frame::PushPromise(_) => self.handle_push_promise_frame(),
6533            Frame::Priority(priority) => self.handle_priority_frame(priority, context, endpoint),
6534            Frame::RstStream(rst_stream) => {
6535                self.handle_rst_stream_frame(rst_stream, context, endpoint)
6536            }
6537            Frame::Settings(settings) => self.handle_settings_frame(settings, context),
6538            Frame::Ping(ping) => self.handle_ping_frame(ping),
6539            Frame::GoAway(goaway) => self.handle_goaway_frame(goaway, context, endpoint),
6540            Frame::WindowUpdate(wu) => self.handle_window_update_frame(wu, context, endpoint),
6541            Frame::PriorityUpdate(pu) => self.handle_priority_update_frame(pu),
6542            Frame::Continuation(_) => {
6543                // Unreachable: standalone CONTINUATION is rejected in
6544                // `handle_header_state` (RFC 9113 §6.10) and in-block
6545                // CONTINUATION is consumed by the inline header-parsing
6546                // path. Keep a defensive fallback that returns
6547                // PROTOCOL_ERROR rather than panicking in debug builds.
6548                self.attribute_bytes_to_overhead();
6549                warn!(
6550                    "{} CONTINUATION frames are handled inline during header parsing",
6551                    log_context!(self)
6552                );
6553                self.goaway(H2Error::ProtocolError)
6554            }
6555            // RFC 9113 §5.5: unknown frame types MUST be ignored and discarded.
6556            // The parser already consumed the payload; attribute the bytes
6557            // to connection-level overhead and continue.
6558            Frame::Unknown(raw) => {
6559                debug!(
6560                    "{} Ignoring unknown H2 frame type {}",
6561                    log_context!(self),
6562                    raw
6563                );
6564                self.attribute_bytes_to_overhead();
6565                MuxResult::Continue
6566            }
6567        };
6568        // Run-to-completion post-condition: the connection-level cross-field
6569        // invariants must hold after every frame is dispatched, on success and
6570        // on the protocol-error paths alike.
6571        #[cfg(debug_assertions)]
6572        self.check_invariants(context);
6573        result
6574    }
6575
6576    /// RFC 9110 §8.6: Content-Length validation must be skipped for responses
6577    /// where the body is absent by definition:
6578    /// - Responses to HEAD requests (any status)
6579    /// - 1xx informational responses
6580    /// - 204 No Content
6581    /// - 304 Not Modified
6582    fn content_length_exempt(
6583        &self,
6584        context: &crate::protocol::kawa_h1::editor::HttpContext,
6585    ) -> bool {
6586        use crate::protocol::kawa_h1::parser::Method;
6587        // HEAD method responses (only relevant when reading backend responses)
6588        if self.position.is_client() && context.method == Some(Method::Head) {
6589            return true;
6590        }
6591        // 1xx, 204, 304 status codes
6592        if let Some(status) = context.status
6593            && ((100..200).contains(&status) || status == 204 || status == 304)
6594        {
6595            return true;
6596        }
6597        false
6598    }
6599
6600    /// What `handle_data_frame` does with the DATA payload of a backend
6601    /// response that has no content by definition (RFC 9110 §6.4.1). Only
6602    /// meaningful when reading a backend response (`Position::Client`): on
6603    /// the server side `context.status` describes the response, not the
6604    /// request being read.
6605    fn backend_response_content(
6606        context: &crate::protocol::kawa_h1::editor::HttpContext,
6607    ) -> BackendResponseContent {
6608        use crate::protocol::kawa_h1::parser::Method;
6609        if matches!(context.status, Some(204 | 304)) {
6610            BackendResponseContent::Forbidden
6611        } else if context.method == Some(Method::Head) {
6612            BackendResponseContent::Discarded
6613        } else {
6614            BackendResponseContent::Forwarded
6615        }
6616    }
6617
6618    fn handle_data_frame<E, L>(
6619        &mut self,
6620        data: parser::Data,
6621        wire_payload_len: u32,
6622        context: &mut Context<L>,
6623        mut endpoint: E,
6624    ) -> MuxResult
6625    where
6626        E: Endpoint,
6627        L: ListenerHandler + L7ListenerHandler,
6628    {
6629        // CVE-2019-9518: track empty DATA frames (no payload, no END_STREAM)
6630        if data.payload.is_empty() && !data.end_stream {
6631            self.flood_detector.record_empty_data_frame();
6632            check_flood_or_return!(self);
6633        }
6634        let Some(global_stream_id) = self.stream_table.get(data.stream_id) else {
6635            // The stream was terminated while data was expected,
6636            // probably due to automatic answer for invalid/unauthorized access.
6637            // RFC 9113 §6.9: we MUST still account for the DATA payload in
6638            // connection-level flow control using the full wire length
6639            // (including pad-length byte and padding), otherwise the window
6640            // shrinks permanently and eventually stalls the connection.
6641            let conn_threshold = self.connection_config.initial_connection_window / 2;
6642            if let Some(increment) = self
6643                .flow_control
6644                .account_received_bytes(wire_payload_len, conn_threshold)
6645            {
6646                self.queue_window_update(0, increment);
6647                self.readiness.arm_writable();
6648            }
6649            self.attribute_bytes_to_overhead();
6650            return MuxResult::Continue;
6651        };
6652        let mut slice = data.payload;
6653        let stream = &mut context.streams[global_stream_id];
6654        // Unpadded application payload size — what is forwarded to the backend
6655        // and counted against Content-Length.
6656        let content_len = slice.len();
6657        // Full wire-payload size (includes pad-length byte and padding).
6658        // RFC 9113 §5.2: padding counts against flow-control windows.
6659        let wire_len = wire_payload_len as usize;
6660        let cl_exempt = self.content_length_exempt(&stream.context);
6661        let content = if self.position.is_client() {
6662            Self::backend_response_content(&stream.context)
6663        } else {
6664            BackendResponseContent::Forwarded
6665        };
6666
6667        // Extract declared content-length and update position-aware data counter
6668        let (data_received, declared_length) = {
6669            let parts = stream.split(&self.position);
6670            *parts.data_received += content_len;
6671            let total = *parts.data_received;
6672            let declared = match parts.rbuffer.body_size {
6673                kawa::BodySize::Length(n) => Some(n),
6674                _ => None,
6675            };
6676            (total, declared)
6677        };
6678
6679        // RFC 9113 §6.9 + §5.2: credit connection-level flow control BEFORE any
6680        // early-return path. Malformed DATA still consumed the peer's send
6681        // window; without crediting it back, repeated bad streams permanently
6682        // shrink the connection window and stall unrelated streams that share
6683        // the same H2 connection. Stream-level credit can stay below — once we
6684        // RST the violating stream, its per-stream window is moot per
6685        // RFC 9113 §6.9 (the receiver discards further frames for the stream).
6686        let conn_threshold = self.connection_config.initial_connection_window / 2;
6687        if let Some(increment) = self
6688            .flow_control
6689            .account_received_bytes(wire_payload_len, conn_threshold)
6690        {
6691            self.queue_window_update(0, increment);
6692        }
6693
6694        // RFC 9110 §15.3.5, §15.4.5, RFC 9113 §8.1.1: a 204 or a 304 cannot
6695        // contain content, so DATA carrying a payload makes it malformed:
6696        // reset the backend stream before the payload is queued, so it never
6697        // reaches the client (an H1 client would read it as the start of the
6698        // next response). `content_length_exempt` skips the length checks
6699        // below for these responses, so they would not catch it. A response
6700        // to HEAD only SHOULD NOT carry content (RFC 9110 §9.3.2): its payload
6701        // is discarded below instead.
6702        if content == BackendResponseContent::Forbidden && content_len > 0 {
6703            error!(
6704                "{} DATA with a {}-byte payload on a {:?} response (RFC 9110 §6.4.1)",
6705                log_context!(self),
6706                content_len,
6707                stream.context.status
6708            );
6709            incr!(names::h2::BODILESS_RESPONSE_DATA_RESET);
6710            if !self.flow_control.pending_window_updates_is_empty() {
6711                self.readiness.arm_writable();
6712            }
6713            let result = self.reset_stream(
6714                data.stream_id,
6715                global_stream_id,
6716                context,
6717                endpoint,
6718                H2Error::ProtocolError,
6719            );
6720            self.remove_dead_stream(data.stream_id, global_stream_id);
6721            return result;
6722        }
6723
6724        // RFC 9113 §8.1.1: if Content-Length is present, total DATA payload
6725        // must not exceed the declared length (check on every frame).
6726        // RFC 9110 §8.6: skip for HEAD/1xx/204/304 responses (body absent by definition).
6727        if !cl_exempt
6728            && let Some(expected) = declared_length
6729            && data_received > expected
6730        {
6731            error!(
6732                "{} Content-Length mismatch: received {} > declared {}",
6733                log_context!(self),
6734                data_received,
6735                expected
6736            );
6737            // Pair WRITABLE arming with the queued connection-level
6738            // WINDOW_UPDATE before returning; otherwise the credit sits
6739            // until the next inbound frame on this connection.
6740            if !self.flow_control.pending_window_updates_is_empty() {
6741                self.readiness.arm_writable();
6742            }
6743            let result = self.reset_stream(
6744                data.stream_id,
6745                global_stream_id,
6746                context,
6747                endpoint,
6748                H2Error::ProtocolError,
6749            );
6750            self.remove_dead_stream(data.stream_id, global_stream_id);
6751            return result;
6752        }
6753
6754        let stream = &mut context.streams[global_stream_id];
6755        self.attribute_bytes_to_stream(&mut stream.metrics);
6756        let stream_state = stream.state;
6757        let is_unlinked = matches!(stream_state, StreamState::Unlinked);
6758        let parts = stream.split(&self.position);
6759        let kawa = parts.rbuffer;
6760        self.position.count_bytes_in(parts.metrics, content_len);
6761
6762        // Stream-level flow control (only if stream is still open).
6763        // Connection-level credit was already applied above the CL check so
6764        // malformed DATA cannot starve the connection window for other streams.
6765        if !data.end_stream {
6766            self.queue_window_update(data.stream_id, wire_payload_len);
6767        }
6768
6769        // If we have pending updates, ensure we get a writable event.
6770        // Must use signal_pending_write() — not just interest.insert() — because
6771        // under edge-triggered epoll the WRITABLE event bit may have been consumed
6772        // by a previous write cycle. Without the event bit set, filter_interest()
6773        // returns 0 and the WINDOW_UPDATEs never get flushed, stalling the client.
6774        if !self.flow_control.pending_window_updates_is_empty() {
6775            self.readiness.arm_writable();
6776        }
6777
6778        // Refresh per-stream idle timer on non-empty DATA.
6779        // Empty DATA frames (CVE-2019-9518 vector) must NOT reset the timer,
6780        // otherwise an attacker can keep a stream alive indefinitely with
6781        // zero-length frames while pinning a MAX_CONCURRENT_STREAMS slot.
6782        if content_len > 0 {
6783            self.stream_table.touch_activity(data.stream_id, self.now);
6784        }
6785
6786        if is_unlinked {
6787            // Backend is gone but client is still sending DATA.
6788            // Discard the data (flow control updates were already
6789            // queued above) to prevent the buffer from filling up.
6790            kawa.storage.clear();
6791            if data.end_stream {
6792                kawa.parsing_phase = kawa::ParsingPhase::Terminated;
6793                self.mark_end_of_stream(stream);
6794            }
6795        } else {
6796            // A response to HEAD has no content (RFC 9110 §9.3.2): its
6797            // payload is never queued, as kawa's H1 parser leaves the bytes
6798            // an H1 backend sends after such a head unread, and its windows
6799            // were credited above.
6800            let discarded = content == BackendResponseContent::Discarded && content_len > 0;
6801            if discarded {
6802                // The frame's wire payload is the tail of the buffer: drop
6803                // it there. Kept behind `storage.head`, it would hold space
6804                // only the frontend consuming a block frees, and no block
6805                // refers to it, so a payload larger than the buffer would
6806                // stop the backend connection from being read before its
6807                // END_STREAM.
6808                debug_assert_eq!(
6809                    kawa.storage.end,
6810                    kawa.storage.head + wire_len,
6811                    "a DATA payload being read is the tail of the stream buffer"
6812                );
6813                kawa.storage.end = kawa.storage.head;
6814            } else {
6815                // Advance storage.head by the full wire payload length so the
6816                // next frame doesn't read stale pad-length+padding bytes.
6817                slice.start = slice.start.saturating_add(kawa.storage.head as u32);
6818                kawa.storage.head += wire_len;
6819            }
6820            // Emit chunk framing for chunked transfer encoding (H2→H1 path).
6821            // H2 converter ignores ChunkHeader and end_chunk Flags, so this is safe for H2→H2.
6822            if kawa.body_size == kawa::BodySize::Chunked && content_len > 0 && !discarded {
6823                let hex_len = {
6824                    let mut buf = Vec::with_capacity(16);
6825                    let _ = write!(buf, "{content_len:x}");
6826                    buf
6827                };
6828                kawa.push_block(kawa::Block::ChunkHeader(kawa::ChunkHeader {
6829                    length: kawa::Store::from_vec(hex_len),
6830                }));
6831            }
6832
6833            if discarded {
6834                debug!(
6835                    "{} DATA payload of {} bytes on a response to HEAD discarded",
6836                    log_context!(self),
6837                    content_len
6838                );
6839            } else {
6840                kawa.push_block(kawa::Block::Chunk(kawa::Chunk {
6841                    data: kawa::Store::Slice(slice),
6842                }));
6843            }
6844
6845            if kawa.body_size == kawa::BodySize::Chunked && content_len > 0 && !discarded {
6846                kawa.push_block(kawa::Block::Flags(kawa::Flags {
6847                    end_body: false,
6848                    end_chunk: true,
6849                    end_header: false,
6850                    end_stream: false,
6851                }));
6852            }
6853
6854            if data.end_stream {
6855                // RFC 9113 §8.1.1: on end_stream, total DATA must equal Content-Length.
6856                // RFC 9110 §8.6: skip for HEAD/1xx/204/304 responses.
6857                if !cl_exempt
6858                    && let Some(expected) = declared_length
6859                    && data_received != expected
6860                {
6861                    error!(
6862                        "{} Content-Length mismatch: received {} != declared {}",
6863                        log_context!(self),
6864                        data_received,
6865                        expected
6866                    );
6867                    let result = self.reset_stream(
6868                        data.stream_id,
6869                        global_stream_id,
6870                        context,
6871                        endpoint,
6872                        H2Error::ProtocolError,
6873                    );
6874                    self.remove_dead_stream(data.stream_id, global_stream_id);
6875                    return result;
6876                }
6877                let is_chunked = kawa.body_size == kawa::BodySize::Chunked;
6878                kawa.push_block(kawa::Block::Flags(kawa::Flags {
6879                    end_body: true,
6880                    end_chunk: is_chunked,
6881                    end_header: false,
6882                    end_stream: true,
6883                }));
6884                kawa.parsing_phase = kawa::ParsingPhase::Terminated;
6885                self.mark_end_of_stream(stream);
6886            }
6887            if let StreamState::Linked(token) = stream_state {
6888                // Mirror of `ConnectionH1::readable`'s close-delimited EOF branch
6889                // for the H2-backend → H2-frontend path: edge-triggered epoll will
6890                // NOT re-fire for bytes we just pushed into stream.back; the
6891                // synthetic event is the only wake path. LIFECYCLE invariant 15.
6892                endpoint.readiness_mut(token).arm_writable();
6893                self.metric_events
6894                    .push(MetricEvent::WritableRearmedPeerData);
6895            }
6896        }
6897        MuxResult::Continue
6898    }
6899
6900    fn handle_headers_frame<E, L>(
6901        &mut self,
6902        headers: Headers,
6903        context: &mut Context<L>,
6904        mut endpoint: E,
6905    ) -> MuxResult
6906    where
6907        E: Endpoint,
6908        L: ListenerHandler + L7ListenerHandler,
6909    {
6910        // HEADERS frames represent real application activity (new request
6911        // or response). Reset the timeout since the peer is actively
6912        // communicating, unlike control frames (PING, WINDOW_UPDATE).
6913        self.arm_timeout();
6914        if !headers.end_headers {
6915            // CVE-2024-27316: only initialize tracking on the very first HEADERS
6916            // fragment, not on re-entries from ContinuationFrame (which call
6917            // handle_frame(Frame::Headers) with the accumulated header block).
6918            self.flood_detector
6919                .begin_header_block_if_new(headers.header_block_fragment.len);
6920            if !self.header_reassembly.is_in_progress() {
6921                // First HEADERS frame of this block: copy its fragment out of
6922                // `zero.storage` now, before the caller's `expect_header()`
6923                // starts overwriting it with the next frame's bytes. A
6924                // re-entry from `(H2State::ContinuationFrame(headers), _)`
6925                // already appended THIS frame's own payload to the
6926                // accumulator before calling back in here (see that match
6927                // arm in `handle_read()`), so there is nothing left to copy.
6928                //
6929                // `data_opt` (bounds-checked), not `data` (panics on OOB):
6930                // `header_block_fragment` is network-facing-derived — the
6931                // parser computes it correctly by construction, so this
6932                // should never actually be out of bounds, but "no panic on
6933                // network-facing input" (CLAUDE.md) applies regardless, and
6934                // the CVE-2024-27316 abort path below this one used to make
6935                // exactly this check before this step replaced its own
6936                // buffer access with `HeaderBlockAccumulator::finish()`
6937                // (owned bytes, no slicing left to bound-check there).
6938                let Some(fragment) = headers
6939                    .header_block_fragment
6940                    .data_opt(self.zero.storage.buffer())
6941                else {
6942                    error!(
6943                        "{} header_block_fragment out of bounds of zero.storage",
6944                        log_context!(self)
6945                    );
6946                    return self.goaway(H2Error::InternalError);
6947                };
6948                self.header_reassembly.begin(fragment);
6949                self.zero.storage.clear();
6950            }
6951            debug!(
6952                "{} FRAGMENT: stream_id={}, accumulated_len={}",
6953                log_context!(self),
6954                headers.stream_id,
6955                self.header_reassembly.len()
6956            );
6957            self.state = H2State::ContinuationHeader(headers);
6958            return MuxResult::Continue;
6959        }
6960        // Header block is complete — reset CONTINUATION counters
6961        self.flood_detector.reset_continuation();
6962        // can this fail?
6963        let stream_id = headers.stream_id;
6964        let Some(global_stream_id) = self.stream_table.get(stream_id) else {
6965            error!(
6966                "{} Handling Headers frame with no attached stream {:#?}",
6967                log_context!(self),
6968                self
6969            );
6970            self.metric_events.push(MetricEvent::HeadersWithoutStream);
6971            self.attribute_bytes_to_overhead();
6972            return self.force_disconnect();
6973        };
6974
6975        // Refresh per-stream idle timer on HEADERS (response headers or trailers
6976        // on an existing stream). Initial HEADERS that create the stream already
6977        // set the timestamp in create_stream().
6978        self.stream_table.touch_activity(stream_id, self.now);
6979
6980        if let Some(priority) = &headers.priority
6981            && self.scheduler.push_priority(stream_id, priority.clone())
6982        {
6983            // This HEADERS frame's own block just completed (single-frame,
6984            // or the final CONTINUATION of a multi-frame sequence) — but
6985            // we're aborting before ever reaching the `finish()` call below.
6986            // If a CONTINUATION sequence fed it, `header_reassembly` is
6987            // still `is_in_progress() == true`; retire it here or it leaks
6988            // that flag into the NEXT HEADERS frame processed on this
6989            // connection, which may belong to a completely different
6990            // stream — that frame's own "read from zero.storage" fast path
6991            // would then be skipped in favour of these stale, already-
6992            // discarded bytes (sozu-proxy/sozu review of e1c3c2fb, B1).
6993            // Regression:
6994            // `a_priority_self_dependency_reset_does_not_leak_the_reassembly_accumulator`.
6995            if self.header_reassembly.is_in_progress() {
6996                self.header_reassembly.finish();
6997            }
6998            self.reset_stream(
6999                stream_id,
7000                global_stream_id,
7001                context,
7002                endpoint,
7003                H2Error::ProtocolError,
7004            );
7005            self.remove_dead_stream(stream_id, global_stream_id);
7006            return MuxResult::Continue;
7007        }
7008
7009        let stream = &mut context.streams[global_stream_id];
7010        self.attribute_bytes_to_stream(&mut stream.metrics);
7011        // The field-block bytes live in one of two places depending on how
7012        // we got here: the owned reassembly accumulator when a CONTINUATION
7013        // sequence fed this HEADERS (`header_reassembly.is_in_progress()`),
7014        // or straight out of `zero.storage` for the single-frame fast path
7015        // — the common case, which never touches the accumulator at all.
7016        // See `h2_header_reassembly.rs`.
7017        let buffer: &[u8] = if self.header_reassembly.is_in_progress() {
7018            self.header_reassembly.data()
7019        } else {
7020            // Invariant: only reachable when nothing is accumulating for
7021            // THIS stream's own block. Every early-return path above this
7022            // point that could have left a CONTINUATION-fed reassembly
7023            // `is_in_progress()` now retires it first (see the priority
7024            // self-dependency branch above — B1 in the review of e1c3c2fb
7025            // was exactly a return that skipped this and leaked the flag
7026            // into the next HEADERS frame's decode).
7027            debug_assert!(!self.header_reassembly.is_in_progress());
7028            headers
7029                .header_block_fragment
7030                .data(self.zero.storage.buffer())
7031        };
7032        let stream = &mut context.streams[global_stream_id];
7033        let parts = &mut stream.split(&self.position);
7034        let was_initial = parts.rbuffer.is_initial();
7035        let elide_x_real_ip = parts.context.elide_x_real_ip;
7036        let (header_events, status) = pkawa::handle_header(
7037            self.hpack.decoder_mut(),
7038            self.scheduler.prioriser_mut(),
7039            stream_id,
7040            parts.rbuffer,
7041            buffer,
7042            headers.end_stream,
7043            parts.context,
7044            self.flood_detector.config().max_header_list_size(),
7045            self.flood_detector.config().max_header_fields(),
7046            elide_x_real_ip,
7047        );
7048        self.metric_events.extend(header_events);
7049        if self.header_reassembly.is_in_progress() {
7050            self.header_reassembly.finish();
7051        }
7052        self.zero.storage.clear();
7053        if let Err((error, global)) = status {
7054            match self.position {
7055                Position::Client(..) => self
7056                    .metric_events
7057                    .push(MetricEvent::BackendHeaderParseError),
7058                Position::Server => self
7059                    .metric_events
7060                    .push(MetricEvent::FrontendHeaderParseError),
7061            }
7062            if global {
7063                error!(
7064                    "{} GOT GLOBAL ERROR WHILE PROCESSING HEADERS",
7065                    log_context!(self)
7066                );
7067                return self.goaway(error);
7068            } else {
7069                let result =
7070                    self.reset_stream(stream_id, global_stream_id, context, endpoint, error);
7071                self.remove_dead_stream(stream_id, global_stream_id);
7072                return result;
7073            }
7074        }
7075        if headers.end_stream {
7076            // RFC 9113 §8.1.1: when END_STREAM arrives via trailers,
7077            // validate that total DATA received matches Content-Length.
7078            // RFC 9110 §8.6: skip for HEAD/1xx/204/304 responses.
7079            if !was_initial && !self.content_length_exempt(&stream.context) {
7080                let parts = stream.split(&self.position);
7081                if let kawa::BodySize::Length(expected) = parts.rbuffer.body_size
7082                    && *parts.data_received != expected
7083                {
7084                    error!(
7085                        "{} Content-Length mismatch on trailers: received {} != declared {}",
7086                        log_context!(self),
7087                        *parts.data_received,
7088                        expected
7089                    );
7090                    let result = self.reset_stream(
7091                        stream_id,
7092                        global_stream_id,
7093                        context,
7094                        endpoint,
7095                        H2Error::ProtocolError,
7096                    );
7097                    self.remove_dead_stream(stream_id, global_stream_id);
7098                    return result;
7099                }
7100            }
7101            self.mark_end_of_stream(stream);
7102        }
7103        if let StreamState::Linked(token) = stream.state {
7104            // Mirror of handle_data_frame's rearm. LIFECYCLE invariant 15.
7105            endpoint.readiness_mut(token).arm_writable();
7106            self.metric_events
7107                .push(MetricEvent::WritableRearmedPeerHeaders);
7108        }
7109        if was_initial && self.position.is_client() {
7110            // The backend's response field block has just been decoded and
7111            // parsed: the client twin of the `is_server()` branch below, on
7112            // the same first-HEADERS edge (`was_initial` is what keeps a
7113            // trailer block out of it). This is the whole of
7114            // `backend_header_time`'s H2 arming — nginx's
7115            // `$upstream_header_time` (sozu-proxy/sozu#426).
7116            //
7117            // Unconditional, including on a 1xx: `ConnectionH2::handle_1xx_reset`
7118            // clears the back buffer once the informational response has been
7119            // forwarded, so the FINAL response returns to the initial parsing
7120            // phase and re-enters this edge, overwriting. That is the response
7121            // `SessionMetrics::backend_stop` also anchors on.
7122            stream.metrics.backend_headers_received();
7123        }
7124        // was_initial prevents trailers from triggering connection
7125        if was_initial && self.position.is_server() {
7126            self.metric_events.push(MetricEvent::RequestStarted);
7127            stream.metrics.service_start();
7128            stream.request_counted = true;
7129            stream.state = StreamState::Link;
7130            context.pending_links.push_back(global_stream_id);
7131        }
7132        MuxResult::Continue
7133    }
7134
7135    fn handle_push_promise_frame(&mut self) -> MuxResult {
7136        self.attribute_bytes_to_overhead();
7137        match self.position {
7138            Position::Client(..) => {
7139                // RFC 9113 §8.4: Server push is deprecated. Sozu never sends
7140                // SETTINGS_ENABLE_PUSH=1, so receiving PUSH_PROMISE is a protocol error.
7141                error!(
7142                    "{} Received PUSH_PROMISE but server push is not supported",
7143                    log_context!(self)
7144                );
7145                self.goaway(H2Error::ProtocolError)
7146            }
7147            Position::Server => {
7148                // Clients must never send PUSH_PROMISE (RFC 9113 §8.4)
7149                error!("{} Received PUSH_PROMISE from client", log_context!(self));
7150                self.goaway(H2Error::ProtocolError)
7151            }
7152        }
7153    }
7154
7155    fn handle_priority_frame<E, L>(
7156        &mut self,
7157        priority: parser::Priority,
7158        context: &mut Context<L>,
7159        endpoint: E,
7160    ) -> MuxResult
7161    where
7162        E: Endpoint,
7163        L: ListenerHandler + L7ListenerHandler,
7164    {
7165        if let Some(global_stream_id) = self
7166            .stream_table
7167            .streams()
7168            .get(&priority.stream_id)
7169            .copied()
7170        {
7171            let stream = &mut context.streams[global_stream_id];
7172            self.attribute_bytes_to_stream(&mut stream.metrics);
7173        } else {
7174            self.attribute_bytes_to_overhead();
7175        }
7176        // Pass 3 Medium #4: standalone PRIORITY frames can arrive for any
7177        // peer-chosen stream ID. Accept only currently-open streams and a
7178        // small idle look-ahead window; everything else is dropped before
7179        // it can feed memory into the priority map.
7180        if self.scheduler.push_priority_guarded(
7181            priority.stream_id,
7182            priority.inner,
7183            self.last_stream_id,
7184            self.stream_table.streams(),
7185        ) {
7186            if let Some(global_stream_id) = self
7187                .stream_table
7188                .streams()
7189                .get(&priority.stream_id)
7190                .copied()
7191            {
7192                let result = self.reset_stream(
7193                    priority.stream_id,
7194                    global_stream_id,
7195                    context,
7196                    endpoint,
7197                    H2Error::ProtocolError,
7198                );
7199                self.remove_dead_stream(priority.stream_id, global_stream_id);
7200                return result;
7201            } else {
7202                // RFC 7540 §5.3.1 makes a self-dependency a stream error, but
7203                // this stream is idle and RFC 9113 §6.4 forbids RST_STREAM on
7204                // an idle stream: the connection error is the only reply.
7205                error!(
7206                    "{} INVALID PRIORITY RECEIVED ON INVALID STREAM",
7207                    log_context!(self)
7208                );
7209                return self.goaway(H2Error::ProtocolError);
7210            }
7211        }
7212        MuxResult::Continue
7213    }
7214
7215    /// RFC 9113 §6.3: a PRIORITY frame with a length other than 5 octets is a
7216    /// stream error of type FRAME_SIZE_ERROR. An open stream is reset; on an
7217    /// idle, closed or already reset stream the frame is dropped, since
7218    /// RST_STREAM must not name an idle stream (§6.4) and PRIORITY changes no
7219    /// stream state. The frame's payload was already consumed.
7220    fn handle_priority_frame_size_error<E, L>(
7221        &mut self,
7222        stream_id: StreamId,
7223        context: &mut Context<L>,
7224        endpoint: E,
7225    ) -> MuxResult
7226    where
7227        E: Endpoint,
7228        L: ListenerHandler + L7ListenerHandler,
7229    {
7230        self.metric_events
7231            .push(MetricEvent::FrameReceived(FrameType::Priority));
7232        self.attribute_bytes_to_overhead();
7233        if let Some(global_stream_id) = self.stream_table.get(stream_id)
7234            && context.streams[global_stream_id].state.is_open()
7235            && !self.stream_table.rst_sent_contains(stream_id)
7236        {
7237            debug!(
7238                "{} PRIORITY frame of invalid length on stream {}, sending RST_STREAM(FRAME_SIZE_ERROR)",
7239                log_context!(self),
7240                stream_id
7241            );
7242            let result = self.reset_stream(
7243                stream_id,
7244                global_stream_id,
7245                context,
7246                endpoint,
7247                H2Error::FrameSizeError,
7248            );
7249            self.remove_dead_stream(stream_id, global_stream_id);
7250            return result;
7251        }
7252        debug!(
7253            "{} Ignoring PRIORITY frame of invalid length on stream {}",
7254            log_context!(self),
7255            stream_id
7256        );
7257        self.flood_detector.record_glitch();
7258        check_flood_or_return!(self);
7259        MuxResult::Continue
7260    }
7261
7262    /// RFC 9218 §7.1: PRIORITY_UPDATE reprioritizes an open or idle-soon
7263    /// stream at the connection level. Decodes the priority field value
7264    /// (same grammar as the `priority` request header, `parse_rfc9218_priority`)
7265    /// and pushes it into the `Prioriser` through the same guarded path used
7266    /// for standalone PRIORITY frames — the guard bounds memory against a
7267    /// client spamming PRIORITY_UPDATE for far-future stream IDs.
7268    ///
7269    /// Prioritized stream ID `0` is a connection-level `PROTOCOL_ERROR`
7270    /// (RFC 9218 §7.1). For any other ID that is not currently open or
7271    /// within the idle look-ahead budget, the update is silently dropped
7272    /// (matches the PRIORITY-frame guard semantics — no state change).
7273    fn handle_priority_update_frame(&mut self, pu: parser::PriorityUpdate) -> MuxResult {
7274        self.attribute_bytes_to_overhead();
7275        if pu.prioritized_stream_id == 0 {
7276            error!(
7277                "{} PRIORITY_UPDATE with prioritized_stream_id=0 (RFC 9218 §7.1)",
7278                log_context!(self)
7279            );
7280            return self.goaway(H2Error::ProtocolError);
7281        }
7282        let (urgency, incremental) = pkawa::parse_rfc9218_priority(&pu.priority_field_value);
7283        let (prev_urgency, _) = self.scheduler.priority(&pu.prioritized_stream_id);
7284        trace!(
7285            "{} PRIORITY_UPDATE stream={} urgency={}->{} incremental={} rearmed_writable=true",
7286            log_context!(self),
7287            pu.prioritized_stream_id,
7288            prev_urgency,
7289            urgency,
7290            incremental
7291        );
7292        let _ = self.scheduler.push_priority_guarded(
7293            pu.prioritized_stream_id,
7294            parser::PriorityPart::Rfc9218 {
7295                urgency,
7296                incremental,
7297            },
7298            self.last_stream_id,
7299            self.stream_table.streams(),
7300        );
7301        // LIFECYCLE invariant 15: reprioritisation only changes ordering for
7302        // the NEXT write pass. Under ET epoll, if finalize_write already
7303        // stripped WRITABLE, the scheduler won't re-run without a synthetic
7304        // wake — pair the interest insert with signal_pending_write.
7305        self.readiness.arm_writable();
7306        self.metric_events
7307            .push(MetricEvent::WritableRearmedPriorityUpdate);
7308        MuxResult::Continue
7309    }
7310
7311    fn handle_rst_stream_frame<E, L>(
7312        &mut self,
7313        rst_stream: parser::RstStream,
7314        context: &mut Context<L>,
7315        mut endpoint: E,
7316    ) -> MuxResult
7317    where
7318        E: Endpoint,
7319        L: ListenerHandler + L7ListenerHandler,
7320    {
7321        // Per-error-code counter for the inbound RST. Emitted before the
7322        // flood-detector trip check so even a connection that gets terminated
7323        // by `handle_flood_violation` shows up in the per-code breakdown
7324        // (the dedicated `h2.flood.violation.rst_stream_*` series tracks the
7325        // mitigation event itself).
7326        self.metric_events
7327            .push(MetricEvent::RstStreamReceived(rst_stream.error_code));
7328        // CVE-2023-44487 Rapid Reset + CVE-2019-9514: track RST_STREAM rate.
7329        self.flood_detector.record_rst_stream_window();
7330        check_flood_or_return!(self);
7331        // Additional CVE-2023-44487 mitigation: connection-lifetime caps on
7332        // RST_STREAM frames received. The per-window counter above
7333        // half-decays, so a patient client could keep resetting forever; the
7334        // lifetime counters cap that, relative to the streams a backend
7335        // answered so benign cancels never accumulate toward a fixed ceiling.
7336        // Streams whose backend response has not yet started count toward a
7337        // much lower "abusive" cap (more than half of the streams, past a
7338        // floor) — the Rapid Reset signature, where the attacker pays one RST
7339        // frame and we pay a backend round-trip for each.
7340        //
7341        // "Response started" here means the Server has begun producing
7342        // response bytes (backend kawa buffer past its initial phase). On a
7343        // backend connection (`Position::Client`) the Rapid Reset signature
7344        // does not exist: Sōzu opened the stream, and a backend resetting it
7345        // before answering — `REFUSED_STREAM` under load, a `CANCEL` from its
7346        // own timeout — costs the backend, not Sōzu. Those resets never count
7347        // as pre-response.
7348        //
7349        // On a frontend connection, a stream reset after its response started
7350        // is an answered stream: it counts toward the caps' denominator
7351        // (`routed_to_a_backend`) before the caps are checked, so a client
7352        // that cancels every stream after its response never has one more
7353        // reset than answered streams.
7354        // A stream refused under flood pressure never got a response:
7355        // resetting it is a pre-response reset, whatever the refusal did to
7356        // the stream table. It also shows the peer does not honour
7357        // REFUSED_STREAM, so the soft refusal ends for this connection: more
7358        // refusals would only multiply the RST_STREAM frames Sōzu sends ahead
7359        // of the cap's GOAWAY.
7360        let refused_stream_reset = self.position.is_server()
7361            && self.stream_table.get(rst_stream.stream_id).is_none()
7362            && self
7363                .flood_refused_streams
7364                .is_some_and(|(first, last)| (first..=last).contains(&rst_stream.stream_id));
7365        if refused_stream_reset {
7366            self.flood_refusal_ignored = true;
7367        }
7368        let (response_started, answered) =
7369            match self.stream_table.streams().get(&rst_stream.stream_id) {
7370                Some(global_stream_id) => {
7371                    let stream = &context.streams[*global_stream_id];
7372                    let started = !stream.back.is_initial();
7373                    (
7374                        self.position.is_client() || started,
7375                        self.position.is_server() && started && routed_to_a_backend(stream),
7376                    )
7377                }
7378                None if refused_stream_reset => (false, false),
7379                // Stream already gone (e.g. closed, not yet registered) —
7380                // treat as response-started to avoid over-counting benign
7381                // races as abusive.
7382                None => (true, false),
7383            };
7384        if let Some(violation) =
7385            self.flood_detector
7386                .record_rst_received(response_started, answered, self.now)
7387        {
7388            return self.handle_flood_violation(violation);
7389        }
7390        // Rapid Reset signature (CVE-2023-44487): a RST that arrives before the
7391        // backend has begun answering. Emitted alongside the per-code counter
7392        // so the SOC can alert on the rate of pre-response RSTs without
7393        // having to differentiate by error code.
7394        if !response_started {
7395            self.metric_events
7396                .push(MetricEvent::RstStreamReceivedBeforeResponse);
7397        }
7398        debug!(
7399            "{} RstStream({} -> {})",
7400            log_context!(self),
7401            rst_stream.error_code,
7402            H2Error::try_from(rst_stream.error_code).map_or("UNKNOWN_ERROR", |e| e.as_str())
7403        );
7404        // Compute totals before removing the stream from the map,
7405        // so the removed stream's bytes are included in the total.
7406        let rst_byte_totals = self.compute_stream_byte_totals(context);
7407        if let Some(global_stream_id) = self
7408            .stream_table
7409            .streams()
7410            .get(&rst_stream.stream_id)
7411            .copied()
7412        {
7413            let stream = &mut context.streams[global_stream_id];
7414            self.attribute_bytes_to_stream(&mut stream.metrics);
7415            let linked_token = stream.linked_token();
7416            let (client_rtt, server_rtt) = self.snapshot_rtts(&endpoint, linked_token);
7417            if let Some(token) = linked_token {
7418                endpoint.end_stream(token, global_stream_id, context);
7419            }
7420            let stream = &mut context.streams[global_stream_id];
7421            match &self.position {
7422                // Inbound RST_STREAM on the backend side terminates the in-flight
7423                // request without going through Connection::end_stream (the normal
7424                // place where Backend.active_requests is decremented), so do the
7425                // bookkeeping explicitly here to avoid leaking load counters.
7426                Position::Client(_, backend, BackendStatus::Connected) => {
7427                    context.record_backend_delta(backend, BackendChange::StreamsEnded(1));
7428                }
7429                Position::Client(..) => {}
7430                Position::Server => {
7431                    self.distribute_overhead(&mut stream.metrics, rst_byte_totals);
7432                    // This is a special case, normally, all stream are terminated by the server
7433                    // when the last byte of the response is written. Here, the reset is requested
7434                    // on the server endpoint and immediately terminates, shortcutting the other path
7435                    stream.metrics.backend_stop();
7436                    let events = stream.generate_access_log(
7437                        true,
7438                        Some("H2::ResetFrame"),
7439                        context.listener.clone(),
7440                        client_rtt,
7441                        server_rtt,
7442                    );
7443                    self.metric_events.extend(events.into_iter().flatten());
7444                    stream.state = StreamState::Recycle;
7445                }
7446            }
7447            // Retire from streams/prioriser/stream_last_activity_at and
7448            // invalidate expect_write/expect_read if they reference this gid.
7449            self.remove_dead_stream(rst_stream.stream_id, global_stream_id);
7450        } else {
7451            self.attribute_bytes_to_overhead();
7452        }
7453        MuxResult::Continue
7454    }
7455
7456    fn handle_settings_frame<L>(
7457        &mut self,
7458        settings: parser::Settings,
7459        context: &mut Context<L>,
7460    ) -> MuxResult
7461    where
7462        L: ListenerHandler + L7ListenerHandler,
7463    {
7464        if settings.ack {
7465            // RFC 9113 §6.5: SETTINGS ACK must have empty payload
7466            if !settings.settings.is_empty() {
7467                error!("{} SETTINGS ACK with non-empty payload", log_context!(self));
7468                return self.goaway(H2Error::FrameSizeError);
7469            }
7470            // RFC 9113 §6.5: peer acknowledged our SETTINGS — clear timeout
7471            self.settings_sent_at = None;
7472            // RFC 7541 §4.2: sync the decoder's max allowed table size with
7473            // what we advertised. Currently a no-op (settings don't change at
7474            // runtime), but guards against future runtime SETTINGS updates.
7475            self.hpack.set_decoder_max_allowed_table_size(
7476                self.local_settings.settings_header_table_size as usize,
7477            );
7478            self.attribute_bytes_to_overhead();
7479            return MuxResult::Continue;
7480        }
7481        // CVE-2019-9515: track SETTINGS frame rate
7482        self.flood_detector.record_settings_frame();
7483        check_flood_or_return!(self);
7484        for setting in settings.settings {
7485            let v = setting.value;
7486            let mut is_error = false;
7487            #[rustfmt::skip]
7488            match setting.identifier {
7489                parser::SETTINGS_HEADER_TABLE_SIZE => {
7490                    // Cap to the configured maximum — a malicious peer can
7491                    // advertise up to 4 GB to inflate HPACK encoder memory.
7492                    let cap = self.flood_detector.config().max_header_table_size();
7493                    let capped = v.min(cap);
7494                    self.peer_settings.settings_header_table_size = capped;
7495                    self.hpack.set_encoder_max_table_size(capped as usize);
7496                    // RFC 7541 §4.2 / §6.3: queue a dynamic-table-size-update
7497                    // HPACK directive for the next header block we emit.
7498                    // Without it, the peer's decoder keeps its previous (possibly
7499                    // larger) table cap and our encoder-side change is silent
7500                    // — conformance suites (h2spec `hpack/4.2`) will flag it.
7501                    self.pending_table_size_update = Some(capped);
7502                },
7503                parser::SETTINGS_ENABLE_PUSH       => { self.peer_settings.settings_enable_push = v == 1;             is_error |= v > 1 },
7504                parser::SETTINGS_MAX_CONCURRENT_STREAMS => { self.peer_settings.settings_max_concurrent_streams = v },
7505                parser::SETTINGS_INITIAL_WINDOW_SIZE    => {
7506                    // RFC 9113 §6.5.2 / §6.9.2: a value above 2^31-1, or one
7507                    // that pushes a stream window past it, is a connection
7508                    // error of type FLOW_CONTROL_ERROR, not PROTOCOL_ERROR.
7509                    if self.update_initial_window_size(v, context) {
7510                        error!("{} INVALID SETTINGS_INITIAL_WINDOW_SIZE {}", log_context!(self), v);
7511                        return self.goaway(H2Error::FlowControlError);
7512                    }
7513                },
7514                parser::SETTINGS_MAX_FRAME_SIZE         => { self.peer_settings.settings_max_frame_size = v;           is_error |= !(MIN_MAX_FRAME_SIZE..MAX_MAX_FRAME_SIZE).contains(&v) },
7515                parser::SETTINGS_MAX_HEADER_LIST_SIZE   => { self.peer_settings.settings_max_header_list_size = v },
7516                parser::SETTINGS_ENABLE_CONNECT_PROTOCOL => { self.peer_settings.settings_enable_connect_protocol = v == 1; is_error |= v > 1 },
7517                parser::SETTINGS_NO_RFC7540_PRIORITIES   => { self.peer_settings.settings_no_rfc7540_priorities = v == 1;   is_error |= v > 1 },
7518                other => { warn!("Unknown setting_id: {}, we MUST ignore this", other); self.flood_detector.record_glitch() },
7519            };
7520            if is_error {
7521                error!("{} INVALID SETTING", log_context!(self));
7522                return self.goaway(H2Error::ProtocolError);
7523            }
7524        }
7525
7526        self.attribute_bytes_to_overhead();
7527
7528        // Enlarge the connection-level receive window for backend H2
7529        // connections (Position::Client). The server side serialises it
7530        // beside its own SETTINGS in the `(ClientSettings, Server)` readable
7531        // arm, but the client needs to do it here after receiving the
7532        // server's initial SETTINGS. Once only: a later SETTINGS frame must
7533        // not grant the surplus again (RFC 9113 §6.9.1).
7534        if self.position.is_client() && !self.connection_window_enlarged {
7535            self.connection_window_enlarged = true;
7536            let increment = self
7537                .connection_config
7538                .initial_connection_window
7539                .saturating_sub(DEFAULT_INITIAL_WINDOW_SIZE);
7540            if increment > 0 {
7541                self.queue_window_update(0, increment);
7542            }
7543            // Do NOT increment flow_control.window here: sending our own
7544            // WINDOW_UPDATE enlarges the peer's send allowance, not ours.
7545            // Our send window is only updated by WINDOW_UPDATEs we receive
7546            // from the peer (RFC 9113 §6.9).
7547        }
7548
7549        self.output.push(&serializer::SETTINGS_ACKNOWLEDGEMENT);
7550        self.queue_output();
7551        MuxResult::Continue
7552    }
7553
7554    /// Hand the frame just queued in [`Self::output`] to the write side.
7555    ///
7556    /// Reading goes on: `zero` is input only, so the next frame header no
7557    /// longer lands on queued output. The frame goes out after everything
7558    /// queued before it, including the rest of a stream frame a partial
7559    /// write cut (#1604), as HAProxy appends it to its single output ring.
7560    fn queue_output(&mut self) {
7561        self.readiness.interest.insert(Ready::WRITABLE);
7562        self.readiness.signal_pending_write();
7563    }
7564
7565    /// How much queued output stops reading, as HAProxy's demux stops on a
7566    /// full `mbuf` (`H2_CF_DEM_MROOM`): one buffer's worth. Every queued
7567    /// control frame answers a frame the peer sent, so a peer that sends
7568    /// while never reading cannot grow [`Self::output`] past this.
7569    fn output_read_cap(&self) -> usize {
7570        self.zero.storage.capacity()
7571    }
7572
7573    /// Drop everything queued in [`Self::output`], and restore reading if the
7574    /// read cap had withdrawn it: the only thing reads waited for is gone.
7575    /// Every site that empties the queue goes through here or through
7576    /// [`Self::consume_output`], so `reads_wait_for_output` never outlives
7577    /// the output it waited on.
7578    fn clear_output(&mut self) {
7579        self.output.clear();
7580        if self.reads_wait_for_output {
7581            self.reads_wait_for_output = false;
7582            self.readiness.interest.insert(Ready::READABLE);
7583        }
7584    }
7585
7586    /// Account for `size` bytes the socket took from [`Self::output`], and
7587    /// restore reading once it drains below [`Self::output_read_cap`].
7588    fn consume_output(&mut self, size: usize) {
7589        let overhead = self.output.consume(size);
7590        self.metric_events
7591            .push(self.position.bytes_out_event(overhead));
7592        self.bytes.overhead_bout += overhead;
7593        if self.reads_wait_for_output && self.output.len() < self.output_read_cap() {
7594            self.reads_wait_for_output = false;
7595            self.readiness.interest.insert(Ready::READABLE);
7596        }
7597    }
7598
7599    fn handle_ping_frame(&mut self, ping: parser::Ping) -> MuxResult {
7600        if ping.ack {
7601            self.attribute_bytes_to_overhead();
7602            return MuxResult::Continue;
7603        }
7604        // CVE-2019-9512: track non-ACK PING frame rate
7605        self.flood_detector.record_ping_frame();
7606        check_flood_or_return!(self);
7607        self.attribute_bytes_to_overhead();
7608        let ping_response_size = serializer::PING_ACKNOWLEDGEMENT_HEADER.len() + 8;
7609        match self.output.push_frames(ping_response_size, |buf| {
7610            serializer::gen_ping_acknowledgement(buf, &ping.payload).map(|(_, size)| size)
7611        }) {
7612            Ok(_) => {
7613                self.metric_events.push(MetricEvent::PingAckFrameSent);
7614            }
7615            Err(error) => {
7616                error!(
7617                    "{} Could not serialize PingFrame: {:?}",
7618                    log_context!(self),
7619                    error
7620                );
7621                return self.force_disconnect();
7622            }
7623        };
7624        self.queue_output();
7625        MuxResult::Continue
7626    }
7627
7628    fn handle_goaway_frame<E, L>(
7629        &mut self,
7630        goaway: parser::GoAway,
7631        context: &mut Context<L>,
7632        mut endpoint: E,
7633    ) -> MuxResult
7634    where
7635        E: Endpoint,
7636        L: ListenerHandler + L7ListenerHandler,
7637    {
7638        self.attribute_bytes_to_overhead();
7639        // A peer sends at most two GOAWAY frames in a graceful close (RFC
7640        // 9113 §6.8). Each one counts toward the glitch budget, so a peer
7641        // repeating GOAWAY on a connection kept open by its in-flight
7642        // streams is bounded, as is the log line below.
7643        self.flood_detector.record_glitch();
7644        check_flood_or_return!(self);
7645        let error_name =
7646            H2Error::try_from(goaway.error_code).map_or("UNKNOWN_ERROR", |e| e.as_str());
7647        if goaway.error_code == H2Error::NoError as u32 {
7648            debug!(
7649                "{} Received GOAWAY: last_stream_id={}, error={}, debug_data={:?}",
7650                log_context!(self),
7651                goaway.last_stream_id,
7652                error_name,
7653                goaway.additional_debug_data
7654            );
7655        } else {
7656            // Peer-originated failure: no variant of H2Error from a peer
7657            // implies a sozu bug. On a backend connection, impact handling
7658            // is separate (retry above `last_stream_id`, RST_STREAM for
7659            // consumed streams) and logs its own details below; a frontend
7660            // connection retires nothing. Either way the summary drops to
7661            // `warn!`.
7662            warn!(
7663                "{} Received GOAWAY: last_stream_id={}, error={}, debug_data={:?}",
7664                log_context!(self),
7665                goaway.last_stream_id,
7666                error_name,
7667                goaway.additional_debug_data
7668            );
7669        }
7670        self.metric_events
7671            .push(MetricEvent::GoAwayReceived(goaway.error_code));
7672        // RFC 9113 §6.8: begin graceful drain.
7673        self.drain.observe_peer_goaway(goaway.last_stream_id);
7674        let peer_last_stream_id = self
7675            .drain
7676            .peer_last_stream_id()
7677            .expect("observe_peer_goaway just recorded this");
7678
7679        // RFC 9113 §6.8: `last_stream_id` bounds the streams the GOAWAY
7680        // RECEIVER initiated. On a frontend connection (Position::Server)
7681        // that is server-initiated streams, which sozu never opens (no
7682        // push), so every stream here was opened by the GOAWAY sender and
7683        // is unaffected: each runs to completion, `draining` refuses any new
7684        // one (`create_stream`), and the connection closes once the last
7685        // completes (the `draining && streams().is_empty()` finalize step).
7686        //
7687        // On a backend connection (Position::Client) the streams are
7688        // sozu-initiated: those with ID > last_stream_id were NOT processed
7689        // by the peer and are retried (StreamState::Link) on a new
7690        // connection; those at or below it run to completion.
7691        // IMPORTANT: do NOT call endpoint.end_stream() here — that would
7692        // remove the stream from the frontend's H2 stream map and send
7693        // RST_STREAM to the client, killing the request instead of retrying it.
7694        let mut retry_streams = Vec::new();
7695        if matches!(self.position, Position::Client(..)) {
7696            for (&stream_id, &global_stream_id) in self.stream_table.streams() {
7697                if stream_id > peer_last_stream_id {
7698                    retry_streams.push((stream_id, global_stream_id));
7699                }
7700            }
7701        }
7702        for (stream_id, global_stream_id) in &retry_streams {
7703            // `endpoint` is the frontend side (`EndpointServer`) only on a
7704            // backend connection: its `readiness_mut` is what the answers
7705            // below must arm so they reach the client.
7706            debug_assert!(
7707                matches!(self.position, Position::Client(..)),
7708                "only a backend connection retires streams on a received GOAWAY"
7709            );
7710            // Remove from reverse index before transitioning away from Linked.
7711            if let StreamState::Linked(token) = context.streams[*global_stream_id].state {
7712                remove_backend_stream(&mut context.backend_streams, token, *global_stream_id);
7713            }
7714            let stream = &mut context.streams[*global_stream_id];
7715            // A stream above `last_stream_id` was not processed (RFC 9113
7716            // §6.8), but it is only replayable if its request can be sent
7717            // again. Once this connection encoded it, `front.out` holds
7718            // frames bound to this connection's stream id and HPACK encoder
7719            // and its header blocks are gone: re-linked, those frames would
7720            // reach another backend connection of this session, possibly one
7721            // this frontend connection's other streams share (sozu#1632).
7722            if stream.front.consumed {
7723                // Request was already sent to this backend — we can't
7724                // replay it. Use the linked token's readiness (via
7725                // endpoint) so the RST_STREAM reaches the client.
7726                debug!(
7727                    "{} GOAWAY: stream {} already consumed, cannot retry",
7728                    log_context!(self),
7729                    stream_id
7730                );
7731                if let StreamState::Linked(token) = stream.state {
7732                    let front_readiness = endpoint.readiness_mut(token);
7733                    forcefully_terminate_answer(stream, front_readiness, H2Error::RefusedStream);
7734                } else {
7735                    warn!(
7736                        "{} GOAWAY: stream {} consumed but not Linked, cannot notify frontend",
7737                        log_context!(self),
7738                        stream_id
7739                    );
7740                }
7741            } else if stream.front_bound_to_backend {
7742                // Encoded for this backend, not sent, not processed: answered
7743                // with a whole 503 response rather than an error-terminated
7744                // one. `forcefully_terminate_answer` leaves an empty response
7745                // kawa in `Error`, which an H2 frontend turns into a
7746                // RST_STREAM but an H1 frontend cannot write at all, so its
7747                // client would wait for its timeout; this core cannot tell
7748                // which frontend it serves. 503, not 502: the backend refused
7749                // the request explicitly and did not process it (RFC 9110
7750                // §15.6.4), which is what a draining GOAWAY means.
7751                debug!(
7752                    "{} GOAWAY: stream {} encoded for this backend and unsent, answering 503",
7753                    log_context!(self),
7754                    stream_id
7755                );
7756                if let StreamState::Linked(token) = stream.state {
7757                    let answers_rc = stream.answers.clone();
7758                    let answers = answers_rc.borrow();
7759                    let front_readiness = endpoint.readiness_mut(token);
7760                    set_default_answer(stream, front_readiness, 503, &answers);
7761                    debug_assert!(
7762                        stream.state == StreamState::Unlinked && !stream.back.is_error(),
7763                        "a refused, unsent request gets a whole answer, never a silent error"
7764                    );
7765                } else {
7766                    warn!(
7767                        "{} GOAWAY: stream {} encoded but not Linked, cannot notify frontend",
7768                        log_context!(self),
7769                        stream_id
7770                    );
7771                }
7772            } else {
7773                // Pre: a stream re-linked here carries no frame of this
7774                // connection (sozu-proxy/sozu#1632).
7775                debug_assert!(
7776                    stream.front.out.is_empty(),
7777                    "a stream re-linked after GOAWAY holds no frame encoded for this connection"
7778                );
7779                stream.state = StreamState::Link;
7780                context.pending_links.push_back(*global_stream_id);
7781            }
7782            // Both retry (!consumed) and terminated (consumed) paths remove the
7783            // stream from self.streams without going through Connection::end_stream,
7784            // so decrement Backend.active_requests here to keep load metrics honest.
7785            if let Position::Client(_, backend, BackendStatus::Connected) = &self.position {
7786                context.record_backend_delta(backend, BackendChange::StreamsEnded(1));
7787            }
7788            // Retire from streams/prioriser/stream_last_activity_at and
7789            // invalidate expect_write/expect_read if they reference this gid.
7790            self.remove_dead_stream(*stream_id, *global_stream_id);
7791        }
7792
7793        // If no active streams remain, close immediately
7794        if self.stream_table.streams().is_empty() {
7795            return self.goaway(H2Error::NoError);
7796        }
7797
7798        // Otherwise, let the remaining streams complete: every stream on a
7799        // frontend connection, those at or below `last_stream_id` on a
7800        // backend one. The connection is closed when all streams finish.
7801        MuxResult::Continue
7802    }
7803
7804    fn handle_window_update_frame<E, L>(
7805        &mut self,
7806        wu: WindowUpdate,
7807        context: &mut Context<L>,
7808        endpoint: E,
7809    ) -> MuxResult
7810    where
7811        E: Endpoint,
7812        L: ListenerHandler + L7ListenerHandler,
7813    {
7814        let stream_id = wu.stream_id;
7815        let increment = wu.increment;
7816
7817        // RFC 9113 §6.9: increment of 0 MUST be treated as an error.
7818        // Connection-level (stream 0) -> connection error (GOAWAY).
7819        // Stream-level -> stream error (RST_STREAM).
7820        if increment == 0 {
7821            if stream_id == 0 {
7822                error!(
7823                    "{} WINDOW_UPDATE with zero increment on connection (stream 0)",
7824                    log_context!(self)
7825                );
7826                return self.goaway(H2Error::ProtocolError);
7827            } else {
7828                error!(
7829                    "{} WINDOW_UPDATE with zero increment on stream {}",
7830                    log_context!(self),
7831                    stream_id
7832                );
7833                if let Some(global_stream_id) = self.stream_table.get(stream_id) {
7834                    let result = self.reset_stream(
7835                        stream_id,
7836                        global_stream_id,
7837                        context,
7838                        endpoint,
7839                        H2Error::ProtocolError,
7840                    );
7841                    self.remove_dead_stream(stream_id, global_stream_id);
7842                    return result;
7843                }
7844                // Stream not in map (already closed) — treat as glitch
7845                self.flood_detector.record_glitch();
7846                check_flood_or_return!(self);
7847                self.attribute_bytes_to_overhead();
7848                return MuxResult::Continue;
7849            }
7850        }
7851
7852        // The parser masks the reserved bit (STREAM_ID_MASK), so increment <=
7853        // 2^31-1 and try_from always succeeds. Use try_from rather than `as` to
7854        // guard against a future parser change that drops the mask.
7855        let increment = i32::try_from(increment).unwrap_or(i32::MAX);
7856        // RFC 9113 §6.9: a non-zero WINDOW_UPDATE increment is in [1, 2^31-1].
7857        // Zero was short-circuited above; this asserts the masked value is a
7858        // legal positive increment before we add it to a window.
7859        debug_assert!(
7860            increment > 0,
7861            "WINDOW_UPDATE increment must be strictly positive at this point (zero handled above)"
7862        );
7863        if stream_id == 0 {
7864            // Count connection-level WINDOW_UPDATEs before touching the window
7865            // so a per-window flood stops us before we pay the arithmetic cost
7866            // on a million-frame burst. Zero-increment frames short-circuited
7867            // above, so every increment here is a legal-looking rate consumer.
7868            self.flood_detector.record_window_update_stream0();
7869            check_flood_or_return!(self);
7870            self.attribute_bytes_to_overhead();
7871            // Window arithmetic + its replenish-invariant asserts live on
7872            // `H2FlowControl` now (see its module doc); this arm keeps the
7873            // frame-dispatch orchestration — flood accounting, logging,
7874            // GOAWAY — which needs `self` (flood_detector, readiness,
7875            // log_context!) that the flow-control module deliberately does
7876            // not have.
7877            match self.flow_control.apply_window_update(increment) {
7878                h2_flow_control::ApplyWindowUpdateOutcome::Applied {
7879                    new_window,
7880                    should_arm_writable,
7881                } => {
7882                    if should_arm_writable {
7883                        self.readiness.arm_writable();
7884                    }
7885                    debug!(
7886                        "{} WINDOW_UPDATE received: stream=0 increment={} new_connection_window={}",
7887                        log_context!(self),
7888                        increment,
7889                        new_window
7890                    );
7891                }
7892                h2_flow_control::ApplyWindowUpdateOutcome::Overflow => {
7893                    error!("{} INVALID WINDOW INCREMENT", log_context!(self));
7894                    return self.goaway(H2Error::FlowControlError);
7895                }
7896            }
7897        } else if let Some(global_stream_id) = self.stream_table.get(stream_id) {
7898            let stream = &mut context.streams[global_stream_id];
7899            self.attribute_bytes_to_stream(&mut stream.metrics);
7900            // RFC 9113 §6.9: only this connection's leg of the stream.
7901            let stream_window = stream.send_window_mut(&self.position);
7902            let stream_window_before = *stream_window;
7903            if let Some(window) = stream_window.checked_add(increment) {
7904                if *stream_window <= 0 && window > 0 {
7905                    self.readiness.arm_writable();
7906                }
7907                *stream_window = window;
7908                // Same replenish invariant as the connection window, applied to
7909                // the per-stream send window (RFC 9113 §6.9.1). Overflow past
7910                // 2^31-1 is rejected by `checked_add` and handled as a
7911                // FLOW_CONTROL_ERROR RST_STREAM below.
7912                debug_assert_eq!(
7913                    window,
7914                    stream_window_before + increment,
7915                    "stream window must increase by exactly the increment"
7916                );
7917                debug_assert!(
7918                    window > stream_window_before,
7919                    "a positive WINDOW_UPDATE must strictly grow the stream window"
7920                );
7921                debug!(
7922                    "{} WINDOW_UPDATE received: stream={} increment={} new_stream_window={}",
7923                    log_context!(self),
7924                    stream_id,
7925                    increment,
7926                    window
7927                );
7928            } else {
7929                let result = self.reset_stream(
7930                    stream_id,
7931                    global_stream_id,
7932                    context,
7933                    endpoint,
7934                    H2Error::FlowControlError,
7935                );
7936                self.remove_dead_stream(stream_id, global_stream_id);
7937                return result;
7938            }
7939        } else {
7940            self.attribute_bytes_to_overhead();
7941            trace!(
7942                "{} Ignoring window update on closed stream {}: {}",
7943                log_context!(self),
7944                stream_id,
7945                increment
7946            );
7947            // Pass 3 Low #5: WINDOW_UPDATE on a closed stream is legal
7948            // (RFC 9113 §6.9.1) but has no useful effect, so a peer that
7949            // keeps sending them is wasting our cycles. Count it as a
7950            // glitch so a flood contributes to `check_flood()` and can
7951            // eventually trigger ENHANCE_YOUR_CALM.
7952            self.flood_detector.record_glitch();
7953            check_flood_or_return!(self);
7954        }
7955        MuxResult::Continue
7956    }
7957
7958    /// Apply a peer's `SETTINGS_INITIAL_WINDOW_SIZE` to every stream window
7959    /// this connection holds (RFC 9113 §6.9.2). `true` when the value is
7960    /// invalid — above 2^31-1, or a change that would push some stream
7961    /// window past it — which the caller answers with
7962    /// GOAWAY(FLOW_CONTROL_ERROR). Every window is checked before any is
7963    /// changed, so a rejected value leaves all of them, and the recorded
7964    /// setting, as they were.
7965    fn update_initial_window_size<L>(&mut self, value: u32, context: &mut Context<L>) -> bool
7966    where
7967        L: ListenerHandler + L7ListenerHandler,
7968    {
7969        if value > FLOW_CONTROL_MAX_WINDOW {
7970            return true;
7971        }
7972        let delta = match i32::try_from(
7973            value as i64 - self.peer_settings.settings_initial_window_size as i64,
7974        ) {
7975            Ok(d) => d,
7976            Err(_) => {
7977                error!("{} initial window size delta overflow", log_context!(self));
7978                return true;
7979            }
7980        };
7981        // RFC 9113 §6.9.2: changes to SETTINGS_INITIAL_WINDOW_SIZE can cause
7982        // stream windows to exceed 2^31-1, which is a flow control error.
7983        // Checked for every stream first, so a rejection changes nothing.
7984        // Only streams owned by this connection, and only this connection's
7985        // leg of each (RFC 9113 §6.9): the other connection's window is its
7986        // own.
7987        let overflows = self
7988            .stream_table
7989            .streams()
7990            .values()
7991            .any(|&global_stream_id| {
7992                context.streams[global_stream_id]
7993                    .send_window_mut(&self.position)
7994                    .checked_add(delta)
7995                    .is_none()
7996            });
7997        if overflows {
7998            return true;
7999        }
8000        let mut open_window = false;
8001        for &global_stream_id in self.stream_table.streams().values() {
8002            let stream_window = context.streams[global_stream_id].send_window_mut(&self.position);
8003            let new_window = *stream_window + delta;
8004            open_window |= *stream_window <= 0 && new_window > 0;
8005            *stream_window = new_window;
8006        }
8007        trace!(
8008            "{} UPDATE INIT WINDOW: {} {} {:?}",
8009            log_context!(self),
8010            delta,
8011            open_window,
8012            self.readiness
8013        );
8014        if open_window {
8015            self.readiness.arm_writable();
8016        }
8017        self.peer_settings.settings_initial_window_size = value;
8018        false
8019    }
8020
8021    /// Take the force-disconnect decision as far as it goes without a socket,
8022    /// and park the rest.
8023    ///
8024    /// Everything this function can settle by itself, it settles here:
8025    /// `H2State::Error`, and on a `Position::Client` connection the backend
8026    /// status and the `HUP` readiness event. What it cannot settle is the
8027    /// `tls_wants_write` answer `h2_close::force_disconnect_action` decides
8028    /// on, because the socket belongs to the shell — so it raises an
8029    /// [`H2ForceDisconnectTarget`] and answers provisionally.
8030    ///
8031    /// The provisional answer is exact everywhere but one case.
8032    /// `Position::Client` always continued. A `Position::Server` whose peer is
8033    /// gone always closes, because `h2_close::force_disconnect_action`
8034    /// answers `CloseSession` on `peer_gone` whatever the socket says. Only a
8035    /// server connection with a live peer genuinely turns on the query, and it
8036    /// answers [`MuxResult::Continue`] until the shell settles it — the
8037    /// conservative direction of the two, since a session held one pass too
8038    /// long is recoverable and a session closed with records still buffered is
8039    /// the truncation `h2_close::force_disconnect_action` exists to prevent.
8040    ///
8041    /// Seventeen sites reach this function and every one returns its answer
8042    /// immediately, so on the ordinary paths no core code runs between the
8043    /// raise and the return of the entry point the shell called.
8044    /// [`Self::goaway`] and `Self::send_initial_goaway` are the exception on
8045    /// their GOAWAY-serialisation failure arms, which `Self::enqueue_rst`'s
8046    /// flood escalation can reach from deep in a frame handler; what those
8047    /// paths can observe in the gap is documented on
8048    /// [`Self::force_disconnect_after_query`].
8049    pub fn force_disconnect(&mut self) -> MuxResult {
8050        self.state = H2State::Error;
8051        // Read ONCE above the `match`, for the borrow reason the
8052        // `tls_wants_write` read that used to sit here had: the client arm
8053        // holds `&mut self.position` for its `status` binding, while this
8054        // query borrows all of `self`. Hoisting is behaviour-neutral — it has
8055        // no side effect and the only field it reads that this function writes
8056        // is `self.state`, assigned above — and it is what lets the raised
8057        // target CARRY the answer instead of re-deriving it later. That
8058        // matters: `peer_gone_after_final_goaway` reads `stream_table` and
8059        // `zero.storage`, and `Self::remove_dead_stream` runs between the
8060        // raise and the settlement on the `Self::reset_stream` paths.
8061        let peer_gone = self.peer_gone_after_final_goaway();
8062        match &mut self.position {
8063            Position::Client(_, _, status) => {
8064                *status = BackendStatus::Disconnecting;
8065                self.readiness.event = Ready::HUP;
8066                self.pending_force_disconnect = Some(H2ForceDisconnectTarget::Client);
8067                MuxResult::Continue
8068            }
8069            Position::Server => {
8070                self.pending_force_disconnect = Some(H2ForceDisconnectTarget::Server { peer_gone });
8071                if peer_gone {
8072                    MuxResult::CloseSession
8073                } else {
8074                    MuxResult::Continue
8075                }
8076            }
8077        }
8078    }
8079
8080    /// Take the [`H2ForceDisconnectTarget`] this connection parked, if it
8081    /// parked one.
8082    ///
8083    /// Taking rather than reading is what makes a second settlement
8084    /// impossible: the next call sees `None` and the shell's helper answers
8085    /// with the result it was already holding.
8086    pub fn poll_force_disconnect(&mut self) -> Option<H2ForceDisconnectTarget> {
8087        self.pending_force_disconnect.take()
8088    }
8089
8090    /// Settle a [`Self::force_disconnect`] with the `tls_wants_write` answer
8091    /// the shell read on its behalf.
8092    ///
8093    /// `tls_wants_write` arrives as this function's own parameter and the
8094    /// target as the other, which is the same discipline
8095    /// [`H2WritableStateTarget::Flush`] keeps by carrying no `bool`: the
8096    /// pre-raise state and the post-raise socket answer are parameters of one
8097    /// function that never read each other's source, and no binding survives
8098    /// in the raising scope for a later edit to point at the wrong question.
8099    /// The query is read fresh here, exactly as [`Self::force_disconnect`]
8100    /// used to read it fresh at all seventeen sites — including the four whose
8101    /// caller already held an answer, which is why none of them passes its own.
8102    ///
8103    /// Between the raise and this call the core cannot change the answer: it
8104    /// holds no socket, so nothing it runs can flush a record. What the gap
8105    /// can change is what the log lines render. On the ordinary paths there is
8106    /// no gap. On [`Self::goaway`]'s serialisation-failure arm, reachable from
8107    /// `Self::enqueue_rst`, `ConnectionH2::remove_dead_stream` and the rest
8108    /// of a frame handler may run first, so `streams=` and `expect_write=`
8109    /// report the connection as the settlement finds it rather than as the
8110    /// raise left it. `peer_gone` is not among them — it is carried.
8111    pub fn force_disconnect_after_query(
8112        &mut self,
8113        target: H2ForceDisconnectTarget,
8114        tls_wants_write: bool,
8115    ) -> MuxResult {
8116        match target {
8117            H2ForceDisconnectTarget::Client => {
8118                debug!(
8119                    "{} H2 force_disconnect client: state={:?}, streams={}, expect_write={:?}, wants_write={}, readiness={:?}",
8120                    log_context!(self),
8121                    self.state,
8122                    self.stream_table.streams().len(),
8123                    self.stream_table.expect_write(),
8124                    tls_wants_write,
8125                    self.readiness
8126                );
8127                MuxResult::Continue
8128            }
8129            // Don't disconnect immediately if rustls still has buffered TLS
8130            // records. Returning CloseSession here triggers shutdown(Write)
8131            // which sends FIN — but any TLS records still in rustls's buffer
8132            // (not yet flushed to the TCP send buffer) are lost, causing the
8133            // client to see "TLS decode error / unexpected eof".
8134            // Instead, keep WRITABLE interest and let the writable path flush.
8135            // The decision itself is `h2_close::force_disconnect_action`,
8136            // exhaustively unit-tested there.
8137            //
8138            // The answer is this function's parameter, and reported by every
8139            // log line here. A literal `wants_write=` in any arm is a second
8140            // copy of a fact the socket already owns, and the closing arm is
8141            // reached with records still pending whenever the peer is gone —
8142            // an operator diagnosing a truncation under HAProxy chaining would
8143            // read the opposite of the socket's state.
8144            H2ForceDisconnectTarget::Server { peer_gone } => {
8145                if h2_close::force_disconnect_action(peer_gone, tls_wants_write)
8146                    == CloseAction::ReArmAndContinue
8147                {
8148                    debug!(
8149                        "{} H2 force_disconnect delaying close: state={:?}, streams={}, expect_write={:?}, wants_write={}, readiness={:?}",
8150                        log_context!(self),
8151                        self.state,
8152                        self.stream_table.streams().len(),
8153                        self.stream_table.expect_write(),
8154                        tls_wants_write,
8155                        self.readiness
8156                    );
8157                    self.readiness.interest = Ready::WRITABLE | Ready::HUP | Ready::ERROR;
8158                    self.ensure_tls_flushed(tls_wants_write);
8159                    MuxResult::Continue
8160                } else {
8161                    debug!(
8162                        "{} H2 force_disconnect closing session: state={:?}, streams={}, expect_write={:?}, wants_write={}, readiness={:?}",
8163                        log_context!(self),
8164                        self.state,
8165                        self.stream_table.streams().len(),
8166                        self.stream_table.expect_write(),
8167                        tls_wants_write,
8168                        self.readiness
8169                    );
8170                    MuxResult::CloseSession
8171                }
8172            }
8173        }
8174    }
8175
8176    /// Reset a stream: tear down kawa state, emit `RST_STREAM` on the wire,
8177    /// and record MadeYouReset accounting.
8178    ///
8179    /// `wire_stream_id` is the on-wire `StreamId`; `stream_id` is the internal
8180    /// `GlobalStreamId` slot. Callers already carry both so we pass them
8181    /// explicitly rather than scanning `self.streams`. The wire id is threaded
8182    /// into `Self::enqueue_rst` which queues the frame for serialisation in
8183    /// `Self::flush_pending_control_frames` on the next writable tick —
8184    /// independent of whether the caller immediately evicts the slot via
8185    /// `remove_dead_stream` (which they usually do). This is what guarantees
8186    /// the RST reaches the peer for malformed HEADERS / flow-control /
8187    /// content-length violations flagged by h2spec 2.0.
8188    pub fn reset_stream<E, L>(
8189        &mut self,
8190        wire_stream_id: StreamId,
8191        stream_id: GlobalStreamId,
8192        context: &mut Context<L>,
8193        mut endpoint: E,
8194        error: H2Error,
8195    ) -> MuxResult
8196    where
8197        E: Endpoint,
8198        L: ListenerHandler + L7ListenerHandler,
8199    {
8200        // Compute totals before taking mutable borrows on the target stream.
8201        let reset_byte_totals = self.compute_stream_byte_totals(context);
8202        context.unlink_stream(stream_id);
8203        let stream = &mut context.streams[stream_id];
8204        trace!(
8205            "{} reset H2 stream {}: {:#?}",
8206            log_context!(self),
8207            stream_id,
8208            stream.context
8209        );
8210        let old_state = std::mem::replace(&mut stream.state, StreamState::Unlinked);
8211        forcefully_terminate_answer(stream, &mut self.readiness, error);
8212        let linked_token = if let StreamState::Linked(token) = old_state {
8213            Some(token)
8214        } else {
8215            None
8216        };
8217        let (client_rtt, server_rtt) = self.snapshot_rtts(&endpoint, linked_token);
8218        if let Some(token) = linked_token {
8219            endpoint.end_stream(token, stream_id, context);
8220        }
8221        // Emit access log for server-side resets on streams that had active requests
8222        if self.position.is_server()
8223            && matches!(old_state, StreamState::Link | StreamState::Linked(_))
8224        {
8225            let stream = &mut context.streams[stream_id];
8226            self.distribute_overhead(&mut stream.metrics, reset_byte_totals);
8227            stream.metrics.backend_stop();
8228            let events = stream.generate_access_log(
8229                true,
8230                Some("H2::Reset"),
8231                context.listener.clone(),
8232                client_rtt,
8233                server_rtt,
8234            );
8235            self.metric_events.extend(events.into_iter().flatten());
8236            stream.metrics.reset();
8237        }
8238        // Queue the RST for wire emission. Independent of the owning stream
8239        // remaining in `self.streams` — callers typically follow this with
8240        // `remove_dead_stream`, which would otherwise evict the slot before
8241        // `write_streams` could run `kawa.prepare` against the converter.
8242        //
8243        // `enqueue_rst` performs every accounting side-effect at queue
8244        // time (per-error counter, global tx counter, CVE-2025-8671
8245        // MadeYouReset cap). Every caller of `reset_stream` answers a frame
8246        // the peer sent (Content-Length mismatch, header parse error,
8247        // PRIORITY self-dependency, zero-increment or overflowing
8248        // WINDOW_UPDATE), so the reset is `RstOrigin::PeerProvoked`; a
8249        // `NoError` reset is still exempt inside the accounting helper.
8250        if let Some(result) = self.enqueue_rst(wire_stream_id, error, RstOrigin::PeerProvoked) {
8251            return result;
8252        }
8253        MuxResult::Continue
8254    }
8255
8256    pub fn end_stream<L>(&mut self, stream_gid: GlobalStreamId, context: &mut Context<L>)
8257    where
8258        L: ListenerHandler + L7ListenerHandler,
8259    {
8260        context.unlink_stream(stream_gid);
8261        let stream_context = context.http_context(stream_gid);
8262        trace!(
8263            "{} end H2 stream {}: {:#?}",
8264            log_context!(self),
8265            stream_gid,
8266            stream_context
8267        );
8268        match self.position {
8269            Position::Client(..) => {
8270                // Resolve the wire StreamId for this gid up front so the
8271                // subsequent cleanup does not hold an iterator borrow on
8272                // `self.streams` while also mutating it.
8273                let wire_stream_id = self
8274                    .stream_table
8275                    .streams()
8276                    .iter()
8277                    .find_map(|(&sid, &gid)| (gid == stream_gid).then_some(sid));
8278                if let Some(id) = wire_stream_id {
8279                    // Only send RST_STREAM if the stream hasn't fully completed.
8280                    // If both request and response are terminated, the stream is
8281                    // already in "closed" state (RFC 9113 §5.1) — sending RST_STREAM
8282                    // on a closed stream would be a protocol error that could cause
8283                    // the H2 peer to close the entire connection.
8284                    //
8285                    // Nor for a stream the backend has never seen: its id is
8286                    // allocated by `start_stream` when the request is linked,
8287                    // but its HEADERS leave with a later write pass, and they
8288                    // may never have (the preface is not out yet, the pass has
8289                    // not run, or the socket took nothing and the block sits
8290                    // parked in `front.out`). On `Position::Client`,
8291                    // `front.consumed` is exactly "a request byte reached this
8292                    // socket", and a stream's first frame is its HEADERS. A
8293                    // RST_STREAM on an idle stream is a connection error
8294                    // (RFC 9113 §5.1, §6.4), and one queued ahead of the
8295                    // preface breaks it (§3.4): either takes down every stream
8296                    // this shared connection carries (sozu-proxy/sozu#1631).
8297                    // The stream is forgotten instead, as hyperium/h2 drops a
8298                    // stream still pending open: its id is burnt, which the
8299                    // next HEADERS on a higher id closes implicitly
8300                    // (RFC 9113 §5.1.1). A block parked unsent is dropped with
8301                    // it and resets the encoder's table on the next pass
8302                    // (`ConnectionH2::parked_header_block`).
8303                    //
8304                    // "Fully completed" means END_STREAM went both ways: the
8305                    // response's was read, and the request's was handed to
8306                    // the output, which flushes in order. A request received
8307                    // whole is not enough: a backend may answer early, before
8308                    // the rest of the body reached it (RFC 9113 §8.1), and a
8309                    // stream forgotten with its tail unsent stays open on the
8310                    // backend, holding one of its MAX_CONCURRENT_STREAMS
8311                    // slots (§5.1.2) for this connection's lifetime.
8312                    let stream = &context.streams[stream_gid];
8313                    let fully_completed = stream.back_received_end_of_stream
8314                        && stream.front.is_terminated()
8315                        && stream.front.is_completed();
8316                    let opened_on_the_wire = stream.front.consumed;
8317                    #[cfg(debug_assertions)]
8318                    let queued_before = self.output.len();
8319                    if opened_on_the_wire
8320                        && !fully_completed
8321                        && !self.stream_table.rst_sent_contains(id)
8322                    {
8323                        // Same path as a PING or SETTINGS ACK: queued whole
8324                        // behind any output already waiting, including the
8325                        // rest of a stream frame a partial write cut, and
8326                        // flushed by the next write pass (#1597, #1604).
8327                        if self
8328                            .output
8329                            .push_frames(RST_STREAM_FRAME_SIZE, |buf| {
8330                                serializer::gen_rst_stream(buf, id, H2Error::Cancel)
8331                                    .map(|(_, size)| size)
8332                            })
8333                            .is_ok()
8334                        {
8335                            self.metric_events
8336                                .push(MetricEvent::RstStreamSent(H2Error::Cancel));
8337                            self.queue_output();
8338                            self.stream_table.rst_sent_mut().insert(id);
8339                        }
8340                    }
8341                    // Retire the stream and invalidate expect_write/expect_read
8342                    // if they still reference this gid — the slot may be popped
8343                    // by `shrink_trailing_recycle` on the next create_stream.
8344                    self.remove_dead_stream(id, stream_gid);
8345                    // Post: a stream that never opened on the wire leaves no
8346                    // frame of its own behind (sozu-proxy/sozu#1631).
8347                    #[cfg(debug_assertions)]
8348                    debug_assert!(
8349                        opened_on_the_wire || self.output.len() == queued_before,
8350                        "no frame is queued for a backend stream whose HEADERS never left"
8351                    );
8352                    debug_assert!(
8353                        !self.stream_table.streams().contains_key(&id),
8354                        "end_stream must retire the backend stream's wire id"
8355                    );
8356                    if context.streams[stream_gid].state != StreamState::Recycle {
8357                        context.streams[stream_gid].state = StreamState::Unlinked;
8358                    }
8359                    return;
8360                }
8361                error!(
8362                    "{} end_stream called for unknown global_stream_id {}",
8363                    log_context!(self),
8364                    stream_gid
8365                );
8366            }
8367            Position::Server => {
8368                let stream = &mut context.streams[stream_gid];
8369                let answers_rc = stream.answers.clone();
8370                match end_stream_decision(stream) {
8371                    EndStreamAction::ForwardTerminated => {
8372                        #[cfg(debug_assertions)]
8373                        context
8374                            .debug
8375                            .push(DebugEvent::Str(format!("Close terminated {stream_gid}")));
8376                        debug!(
8377                            "{} CLOSING H2 TERMINATED STREAM {} {:?}",
8378                            log_context!(self),
8379                            stream_gid,
8380                            stream
8381                        );
8382                        stream.state = StreamState::Unlinked;
8383                        self.readiness.arm_writable();
8384                        context.debug.set_interesting(true);
8385                    }
8386                    EndStreamAction::CloseDelimited
8387                        if stream.back.body_size != kawa::BodySize::Empty =>
8388                    {
8389                        // A `Content-Length` body short of its length, or a chunked body cut before
8390                        // its `0\r\n\r\n` or in its trailers, is truncated (RFC 9112 §6.3, §7.1,
8391                        // sozu-proxy/sozu#1633): RST_STREAM, never END_STREAM, as does
8392                        // `ConnectionH1::terminate_close_delimited`. The blocks not yet encoded go
8393                        // with it, among them a trailer block still waiting for
8394                        // its closing flags, which the converter would otherwise
8395                        // wait on forever; none of them touched the HPACK table
8396                        // (sozu-proxy/sozu#1627).
8397                        warn!(
8398                            "{} H1 backend closed a {:?} response mid-body on H2 stream {}: emitting RST_STREAM",
8399                            log_context!(self),
8400                            stream.back.body_size,
8401                            stream_gid
8402                        );
8403                        incr!(names::h1::BACKEND_EOF_BEFORE_MESSAGE_COMPLETE);
8404                        stream.back.blocks.clear();
8405                        stream
8406                            .back
8407                            .parsing_phase
8408                            .error(kawa::ParsingErrorKind::Processing {
8409                                message: "INTERNAL_ERROR",
8410                            });
8411                        stream.state = StreamState::Unlinked;
8412                        self.readiness.arm_writable();
8413                        context.debug.set_interesting(true);
8414                        debug_assert!(
8415                            stream.back.is_error() && stream.back.blocks.is_empty(),
8416                            "a truncated response ends in Error with nothing left to encode"
8417                        );
8418                    }
8419                    EndStreamAction::CloseDelimited => {
8420                        debug!(
8421                            "{} CLOSE DELIMITED H2 STREAM {} {:?}",
8422                            log_context!(self),
8423                            stream_gid,
8424                            stream
8425                        );
8426                        stream.back.push_block(kawa::Block::Flags(kawa::Flags {
8427                            end_body: true,
8428                            end_chunk: false,
8429                            end_header: false,
8430                            end_stream: true,
8431                        }));
8432                        stream.back.parsing_phase = kawa::ParsingPhase::Terminated;
8433                        stream.state = StreamState::Unlinked;
8434                        self.readiness.arm_writable();
8435                        context.debug.set_interesting(true);
8436                    }
8437                    EndStreamAction::ForwardUnterminated => {
8438                        #[cfg(debug_assertions)]
8439                        context
8440                            .debug
8441                            .push(DebugEvent::Str(format!("Close unterminated {stream_gid}")));
8442                        debug!(
8443                            "{} CLOSING H2 UNTERMINATED STREAM {} {:?}",
8444                            log_context!(self),
8445                            stream_gid,
8446                            stream
8447                        );
8448                        forcefully_terminate_answer(
8449                            stream,
8450                            &mut self.readiness,
8451                            H2Error::InternalError,
8452                        );
8453                        context.debug.set_interesting(true);
8454                    }
8455                    EndStreamAction::SendDefault(status) => {
8456                        #[cfg(debug_assertions)]
8457                        context.debug.push(DebugEvent::Str(format!(
8458                            "Can't retry, send {status} on {stream_gid}"
8459                        )));
8460                        let answers = answers_rc.borrow();
8461                        set_default_answer(stream, &mut self.readiness, status, &answers);
8462                    }
8463                    EndStreamAction::ReplayOnFreshBackend => {
8464                        // Reachable only through an H1 BACKEND behind this H2
8465                        // frontend: `reused_from_pool` — and so the captured
8466                        // request — is set by `ConnectionH1::start_stream`
8467                        // alone, and an H2 backend never fills it. The action
8468                        // is handled here rather than merged into
8469                        // `SendDefault` so the H2 frontend re-routes the same
8470                        // stale-upstream race the H1 frontend does, with the
8471                        // same bytes on the wire.
8472                        match stream.queue_upstream_replay() {
8473                            Some(len) => {
8474                                debug!(
8475                                    "{} H2 REPLAY {} request bytes on a fresh backend",
8476                                    log_context!(self),
8477                                    len
8478                                );
8479                                // Deliberately still a metric macro, and the
8480                                // only one left in this core. It is the one
8481                                // labelled site: carrying its cluster and
8482                                // backend labels in a `MetricEvent` means
8483                                // owned `Option<String>`s, which measures at
8484                                // `size_of::<MetricEvent>()` 24 -> 48 bytes —
8485                                // paid by every queued event, for a site that
8486                                // fires only on a stale-upstream retry — and
8487                                // costs the enum its `Copy`. Recording it here
8488                                // is the cheaper half of that trade, not an
8489                                // oversight.
8490                                incr!(
8491                                    names::backend::RETRY_STALE_UPSTREAM,
8492                                    stream.context.cluster_id.as_deref(),
8493                                    stream.context.backend_id.as_deref(),
8494                                    stream.context.cluster_metrics_incarnation()
8495                                );
8496                                stream.state = StreamState::Link;
8497                                context.pending_links.push_back(stream_gid);
8498                            }
8499                            None => {
8500                                error!(
8501                                    "{} replay selected with no captured request",
8502                                    log_context!(self)
8503                                );
8504                                let answers = answers_rc.borrow();
8505                                set_default_answer(stream, &mut self.readiness, 502, &answers);
8506                            }
8507                        }
8508                    }
8509                    EndStreamAction::Reconnect => {
8510                        debug!("{} H2 RECONNECT", log_context!(self));
8511                        #[cfg(debug_assertions)]
8512                        context
8513                            .debug
8514                            .push(DebugEvent::Str(format!("Retry {stream_gid}")));
8515                        stream.state = StreamState::Link;
8516                        context.pending_links.push_back(stream_gid);
8517                    }
8518                }
8519            }
8520        }
8521    }
8522
8523    pub fn start_stream<L>(&mut self, stream: GlobalStreamId, context: &mut Context<L>) -> bool
8524    where
8525        L: ListenerHandler + L7ListenerHandler,
8526    {
8527        // Entry point: a backend connection reaches this from the router
8528        // without necessarily having had `readable`/`writable` called on it
8529        // this pass, so adopt the mux's snapshot before arming the new
8530        // stream's liveness deadline below.
8531        self.now = context.now;
8532        // RFC 9113 §6.8: reject new streams on a draining connection
8533        if self.drain.draining() {
8534            error!(
8535                "{} Cannot open new stream on draining connection (stream {})",
8536                log_context!(self),
8537                stream
8538            );
8539            return false;
8540        }
8541        // RFC 9113 §5.1.2: respect peer's max concurrent streams limit
8542        if self.stream_table.len() >= self.peer_settings.settings_max_concurrent_streams as usize {
8543            error!(
8544                "{} Cannot open new stream: active={} >= peer max_concurrent_streams={}",
8545                log_context!(self),
8546                self.stream_table.len(),
8547                self.peer_settings.settings_max_concurrent_streams
8548            );
8549            return false;
8550        }
8551        trace!(
8552            "{} start new H2 stream {} {:?}",
8553            log_context!(self),
8554            stream,
8555            self.readiness
8556        );
8557        // sozu-proxy/sozu#1632, defense in depth: a request whose front
8558        // already holds output was prepared for another connection — frames
8559        // with its stream id and HPACK state, or H1 bytes — and its blocks
8560        // are gone, so this connection could neither send nor re-encode it.
8561        // `end_stream_decision` and `handle_goaway_frame` never link such a
8562        // request; this refuses one that a future path would, before an id is
8563        // allocated. The caller answers the client with an error.
8564        if !context.streams[stream].front.out.is_empty() {
8565            error!(
8566                "{} Cannot open stream {}: its request was already encoded for another connection",
8567                log_context!(self),
8568                stream
8569            );
8570            return false;
8571        }
8572        let Some(stream_id) = self.new_stream_id() else {
8573            // Pass 4 Medium #5: the client-initiated stream-ID space
8574            // (31 bits, odd only) is exhausted. The backend is now useless
8575            // for new requests — gracefully drain it. Without this
8576            // transition, the Connection lingers in `Connected` state and
8577            // every subsequent request returns 503 because `start_stream`
8578            // keeps returning false.
8579            //
8580            // The session envelope is hoisted to a local because the
8581            // `match &mut self.position` below holds a mutable borrow on
8582            // `self.position`, and `log_context!(self)` reads that field
8583            // for its `position={...}` slot — calling the macro inside the
8584            // match arms would conflict with the active borrow. The
8585            // bidirectional regression guard in `lib/tests/log_layout.rs`
8586            // (and the matching scanner in `lib/build.rs`) recognises this
8587            // shape by scanning backward as well as forward from each log
8588            // call.
8589            let context = log_context!(self);
8590            match &mut self.position {
8591                Position::Client(cluster_id, backend, status) => {
8592                    let backend_addr = backend.address;
8593                    let cluster = cluster_id.clone();
8594                    info!(
8595                        "{} H2 backend stream IDs exhausted (cluster={}, backend={:?}) — draining",
8596                        context, cluster, backend_addr
8597                    );
8598                    *status = BackendStatus::Disconnecting;
8599                }
8600                Position::Server => {
8601                    error!(
8602                        "{} H2 server stream IDs exhausted — sending graceful GOAWAY",
8603                        context
8604                    );
8605                }
8606            }
8607            self.graceful_goaway(self.now);
8608            return false;
8609        };
8610        // RFC 9113 §6.9: the stream's send window on this connection starts
8611        // at the backend's own SETTINGS_INITIAL_WINDOW_SIZE (the 65535
8612        // default until its SETTINGS arrive), whatever the client granted on
8613        // the frontend connection and whatever a previous request on the
8614        // slot left.
8615        *context.streams[stream].send_window_mut(&self.position) =
8616            i32::try_from(self.peer_settings.settings_initial_window_size).unwrap_or(i32::MAX);
8617        // Pre: the backend leg of the slot starts fresh, whether it comes
8618        // from `Context::create_stream` or from an H1 frontend's keep-alive
8619        // reset (sozu-proxy/sozu#1781): nothing has been received on it yet,
8620        // or `handle_read` would refuse the response as arriving on a closed
8621        // stream. The frontend leg may legitimately be done already: an H2
8622        // client's request ends with END_STREAM before the backend is
8623        // dialled.
8624        debug_assert!(
8625            !context.streams[stream].back_received_end_of_stream
8626                && context.streams[stream].back_data_received == 0,
8627            "a stream must open on a backend with a fresh backend leg"
8628        );
8629        self.stream_table.register(stream_id, stream, self.now);
8630        // Sōzu opened this stream; the backend peer cannot inflate the count.
8631        self.flood_detector.record_stream_opened();
8632        self.readiness.arm_writable();
8633        true
8634    }
8635}
8636
8637/// A [`ConnectionH2`] and the socket it speaks through.
8638///
8639/// The split issue #1339 Q10 decided, in the shape it decided: the core is
8640/// byte-in / byte-out and knows nothing about sockets, while everything that
8641/// reaches one lives here. `Connection::H2` holds this rather than the core,
8642/// so `Connection<Front: SocketHandler>` keeps its bound and `Router::backends`
8643/// keeps its declared type — the parameter did not leave the mux, it moved off
8644/// the state machine.
8645///
8646/// Both halves sit in this module, so this impl reaches the core's private
8647/// fields exactly as the methods it inherited used to. Nothing was widened to
8648/// `pub` to let the socket keep working; the `pub` surface that did grow
8649/// exists for a driver OUTSIDE this crate — `sim/tests/h2_simulation.rs` is
8650/// the first, and it builds its own shell over the same poll/handle pairs
8651/// this one uses.
8652pub struct H2Shell<Front: SocketHandler> {
8653    /// The socket-free state machine.
8654    pub core: ConnectionH2,
8655    /// The only socket in the H2 frontend or backend path.
8656    pub socket: Front,
8657    /// `write_streams`' vectored-write scratch, kept for the connection's
8658    /// lifetime so a transmitting pass reuses its capacity instead of
8659    /// allocating it. Empty between two `h2_transmit::gather` /
8660    /// `h2_transmit::confirm` brackets; private so that bracket cannot be
8661    /// split from outside this module.
8662    io_slices: Vec<IoSlice<'static>>,
8663    /// This connection's one sample of [`Self::socket`]'s round-trip time:
8664    /// `None` until the first stream logged on the connection asks for it
8665    /// through [`ShellEndpoint::local_rtt`] (a frontend's `client_rtt`) or
8666    /// `Connection::rtt` (a backend's `server_rtt`), then `Some(sample)` for
8667    /// the rest of the connection's life — see
8668    /// [`memoized_rtt`](super::memoized_rtt). A `Cell` because
8669    /// `Endpoint::local_rtt` takes `&self`.
8670    pub(super) local_rtt: Cell<Option<Option<Duration>>>,
8671    /// A write to the client failed (reset, broken pipe or another socket
8672    /// error): nothing more can be flushed to it, so the connection reports
8673    /// no pending write and `Self::writable` closes the session instead of
8674    /// waiting for a flush that cannot happen (sozu-proxy/sozu#1792).
8675    write_failed: bool,
8676}
8677
8678/// Renders the core and nothing else.
8679///
8680/// Deliberately NOT derived. A derive would render `socket` through
8681/// `Front`'s own `Debug`, which for a `mio::net::TcpStream` is a local
8682/// address, a peer address and a file descriptor — precisely what
8683/// [`ConnectionH2`]'s own `Debug` stopped rendering, and for the same reasons
8684/// it gives. Wrapping the core in a new type must not hand that back through
8685/// the wrapper.
8686impl<Front: SocketHandler> std::fmt::Debug for H2Shell<Front> {
8687    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
8688        self.core.fmt(f)
8689    }
8690}
8691
8692/// Symmetric tear-down for the four aggregate gauges
8693/// `ConnectionH2::gauge_connection_state` feeds — the three
8694/// `h2.connection.*` metrics and `h2.streams.ready_incremental.by_urgency`:
8695/// whatever positive contribution this connection made is subtracted back out
8696/// when the connection is dropped, and the whole event queue is flushed with
8697/// it.
8698///
8699/// Using `Drop` (rather than wiring decrements into every close path —
8700/// `graceful_goaway`, `force_disconnect`, `handle_goaway_frame`, `Mux::close`,
8701/// stream-id exhaustion, panic-unwind) is what guarantees the gauge is
8702/// arithmetically symmetric regardless of which path teardown took. Past
8703/// underflow incidents (commits ff401b54, aadb3fa4) were increment/decrement
8704/// asymmetries. `ff401b54` carried both shapes at once: a `-1` for streams that
8705/// never ran the `+1`, AND a `-1` that ran twice on one stream (100-Continue,
8706/// and an H2 reset followed by close). `aadb3fa4` was the doubled `-1` alone,
8707/// let through by an early return. `Drop` closes both shapes — it is the sole
8708/// decrement site, and taking `last_gauge_snapshot` makes a second call a
8709/// no-op.
8710///
8711/// It sits on the **shell** rather than on [`ConnectionH2`] because
8712/// `record_metric` writes the worker-local `METRICS` aggregator, which is
8713/// exactly the ambient reach the core gave up. RAII is not given up with it:
8714/// the shell owns the core, so the core cannot outlive this, and because
8715/// `H2Shell` now implements `Drop` the compiler additionally refuses to move
8716/// `core` out of it. If this impl were lost, every live connection's
8717/// contribution would leak upward permanently, with no resync and no
8718/// underflow to notice — which is what
8719/// `dropping_a_connection_releases_its_ready_incremental_contribution` pins.
8720/// The [`Endpoint`] the shell hands its core: every call forwards to `inner`,
8721/// except [`Endpoint::local_rtt`], which the shell answers from its own socket.
8722///
8723/// The core asks for the local round-trip time only when it logs a stream
8724/// (`ConnectionH2::snapshot_rtts`), so the `getsockopt(TCP_INFO)` happens then
8725/// and only then, once per connection: the first ask samples and stores into
8726/// `sample`, every later one on the connection reuses it. Built per core
8727/// call from disjoint borrows of the shell's fields, so the core never holds
8728/// the socket. Issue #1590.
8729struct ShellEndpoint<'a, E, Front> {
8730    inner: &'a mut E,
8731    socket: &'a Front,
8732    sample: &'a Cell<Option<Option<Duration>>>,
8733}
8734
8735impl<'a, E, Front> ShellEndpoint<'a, E, Front> {
8736    fn new(
8737        inner: &'a mut E,
8738        socket: &'a Front,
8739        sample: &'a Cell<Option<Option<Duration>>>,
8740    ) -> Self {
8741        ShellEndpoint {
8742            inner,
8743            socket,
8744            sample,
8745        }
8746    }
8747}
8748
8749impl<E: Endpoint, Front: SocketHandler> std::fmt::Debug for ShellEndpoint<'_, E, Front> {
8750    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
8751        f.debug_struct("ShellEndpoint")
8752            .field("inner", &self.inner)
8753            .field("sample", &self.sample.get())
8754            .finish()
8755    }
8756}
8757
8758impl<E: Endpoint, Front: SocketHandler> Endpoint for ShellEndpoint<'_, E, Front> {
8759    fn readiness(&self, token: mio::Token) -> &Readiness {
8760        self.inner.readiness(token)
8761    }
8762    fn readiness_mut(&mut self, token: mio::Token) -> &mut Readiness {
8763        self.inner.readiness_mut(token)
8764    }
8765    fn peer_rtt(&self, token: mio::Token) -> Option<Duration> {
8766        self.inner.peer_rtt(token)
8767    }
8768    fn local_rtt(&self) -> Option<Duration> {
8769        super::memoized_rtt(self.sample, self.socket.socket_ref())
8770    }
8771    fn end_stream<L: ListenerHandler + L7ListenerHandler>(
8772        &mut self,
8773        token: mio::Token,
8774        stream: GlobalStreamId,
8775        context: &mut Context<L>,
8776    ) {
8777        self.inner.end_stream(token, stream, context)
8778    }
8779    fn start_stream<L: ListenerHandler + L7ListenerHandler>(
8780        &mut self,
8781        token: mio::Token,
8782        stream: GlobalStreamId,
8783        context: &mut Context<L>,
8784    ) -> bool {
8785        self.inner.start_stream(token, stream, context)
8786    }
8787}
8788
8789impl<Front: SocketHandler> Drop for H2Shell<Front> {
8790    fn drop(&mut self) {
8791        self.core.release_connection_gauges();
8792        self.record_pending_metrics();
8793    }
8794}
8795
8796impl<Front: SocketHandler> H2Shell<Front> {
8797    /// The TLS layer is holding encrypted records — handshake, alert, session
8798    /// ticket or already-accepted application data — that it must push before
8799    /// this connection can call a pass finished.
8800    ///
8801    /// **Not "the socket is writable".** The name of the underlying
8802    /// [`SocketHandler`] method says socket, and that is what made this look
8803    /// like an I/O question for as long as it was spelled out at every site.
8804    /// It is a question about a buffer that happens to live behind the socket:
8805    /// `FrontRustls` is the only production handler that overrides it, the
8806    /// trait's default answer is `false`, and `Router::backends` is declared
8807    /// `Connection<SessionTcpStream>` whatever the frontend is — so every
8808    /// backend H2 connection already resolves every one of these queries
8809    /// statically to `false`. The same core is monomorphised twice today, once
8810    /// with TLS and once without, which is the evidence that the coupling is
8811    /// incidental rather than structural.
8812    ///
8813    /// This is a seam, not an abbreviation. It is the single reach where
8814    /// `ConnectionH2` asks that question, and the whole of what a byte-in /
8815    /// byte-out core has to receive as an input instead of querying: the shell
8816    /// that owns the socket is the only layer that can answer it, and it is
8817    /// the layer this method's body moves to. Until then the body is the
8818    /// delegation it always was and the answer is bit-identical.
8819    ///
8820    /// The query is free of side effects, so a caller that needs it twice
8821    /// around a [`Self::flush_tls_records`] is asking two genuinely different
8822    /// questions — "does rustls hold records" and "did the kernel take them" —
8823    /// and a caller that needs it twice with nothing in between should bind it
8824    /// once instead. `h2_close::TlsFlushPhase` names the first case;
8825    /// [`ConnectionH2::force_disconnect`] is the second.
8826    fn tls_wants_write(&self) -> bool {
8827        self.socket.socket_wants_write()
8828    }
8829
8830    /// Ask the TLS layer to push whatever it has buffered towards the kernel,
8831    /// offering it no new application bytes.
8832    ///
8833    /// The empty slice is the whole point: this is a flush, not a write. It is
8834    /// the only *action* among the three TLS seams, which is why the four call
8835    /// sites are the four places where a byte-in / byte-out core must hand
8836    /// control back to its shell rather than take an input — the answer to
8837    /// [`Self::tls_wants_write`] changes across this call, so no value captured
8838    /// before it can stand in for the query after it.
8839    ///
8840    /// Three of the four have made that move and all three are in the shell
8841    /// impl: [`Self::write_streams`] performs it for
8842    /// [`H2FinalizeTarget::Flush`], outside the [`ConnectionH2::finalize_write`] that
8843    /// used to run it inline, and [`Self::writable`] performs both of the
8844    /// connection-level write pass's — its unconditional preamble, and the
8845    /// second one [`H2WritableStateTarget::Flush`] asks for on behalf of the
8846    /// `H2State::GoAway` arm now in [`ConnectionH2::dispatch_writable_state`]. The
8847    /// fourth is [`Self::flush_output_buffer`].
8848    ///
8849    /// Returns the handler's `(size, SocketResult)` unchanged. Three of the
8850    /// four callers discard both and re-query [`Self::tls_wants_write`]
8851    /// instead — deliberately, since a flush that moved bytes may still leave
8852    /// records behind — and only [`Self::flush_output_buffer`] consumes the
8853    /// status, through `update_readiness_after_write`.
8854    fn flush_tls_records(&mut self) -> (usize, SocketResult) {
8855        let (size, status) = self.socket.socket_write(&[]);
8856        self.core.note_write_status(status);
8857        self.record_write(status);
8858        (size, status)
8859    }
8860
8861    /// Remember a failed write to the client: an H2 frontend whose socket
8862    /// answered `Error` or `Closed` can flush nothing more. A backend keeps
8863    /// its own failure handling.
8864    fn record_write(&mut self, status: SocketResult) {
8865        if self.core.position.is_server()
8866            && matches!(status, SocketResult::Error | SocketResult::Closed)
8867        {
8868            self.write_failed = true;
8869        }
8870    }
8871
8872    /// Start the TLS `close_notify` handshake, generating the records that
8873    /// [`Self::tls_wants_write`] will then report as pending.
8874    ///
8875    /// Idempotence is the caller's business, not this seam's:
8876    /// [`Self::initiate_close_notify`] gates it on `close_notify_sent` so a
8877    /// second call cannot queue a second alert.
8878    fn begin_tls_close(&mut self) {
8879        self.socket.socket_close()
8880    }
8881
8882    /// Start TLS close_notify on the frontend and keep the session alive until
8883    /// rustls has flushed the generated records.
8884    pub fn initiate_close_notify(&mut self) -> bool {
8885        if !self.core.position.is_server()
8886            || matches!(
8887                self.core.state,
8888                H2State::ClientPreface | H2State::ClientSettings | H2State::ServerSettings
8889            )
8890        {
8891            return false;
8892        }
8893        if !self.core.close_notify_sent {
8894            trace!("{} H2 initiating CLOSE_NOTIFY", log_context!(self.core));
8895            self.begin_tls_close();
8896            self.core.close_notify_sent = true;
8897        }
8898        let tls_wants_write = self.tls_wants_write();
8899        if tls_wants_write {
8900            self.core.readiness.interest = Ready::WRITABLE | Ready::HUP | Ready::ERROR;
8901            self.core.ensure_tls_flushed(tls_wants_write);
8902            true
8903        } else {
8904            false
8905        }
8906    }
8907
8908    /// Returns `true` if there is data queued waiting to be flushed:
8909    /// - The ordered output queue: control frames (GOAWAY, SETTINGS ACK,
8910    ///   etc.) and the rest of a stream frame a stalled write cut
8911    /// - A stream parked by a stalled write (`expect_write`)
8912    /// - Encrypted TLS records in rustls's output buffer not yet flushed to TCP
8913    ///
8914    /// The TLS check is critical: `shutting_down()` uses this to prevent
8915    /// premature session close while response DATA is still in rustls's
8916    /// buffer (accepted by `socket_write_vectored` but not yet on the wire).
8917    ///
8918    /// Does NOT check per-stream `back.out`/`back.blocks`; use
8919    /// [`Self::has_pending_write_full`] on paths that must honour
8920    /// LIFECYCLE invariant 16 (e.g. shutdown-drain).
8921    pub fn has_pending_write(&self) -> bool {
8922        !self.write_failed && self.core.has_pending_write_with(self.tls_wants_write())
8923    }
8924
8925    /// Connection-level [`Self::has_pending_write`] extended with a per-stream
8926    /// back-buffer probe (LIFECYCLE §9 invariant 16). Used by shutdown-drain
8927    /// paths that must not close while any open stream still has outbound
8928    /// kawa bytes queued — a voluntary scheduler yield can leave `back.out`
8929    /// or `back.blocks` non-empty without `expect_write` being set.
8930    pub fn has_pending_write_full<L>(&self, context: &Context<L>) -> bool
8931    where
8932        L: ListenerHandler + L7ListenerHandler,
8933    {
8934        !self.write_failed
8935            && (self.has_pending_write()
8936                || any_stream_has_pending_back(self.core.stream_table.streams(), &context.streams))
8937    }
8938
8939    /// Flush the ordered output queue to the socket, counting its own bytes
8940    /// as connection overhead.
8941    ///
8942    /// Returns `true` if the socket stalled (WouldBlock / zero-length write),
8943    /// meaning the caller should stop writing and wait for the next writable event.
8944    /// Returns `false` when the queue has been fully drained.
8945    fn flush_output_to_socket(&mut self) -> bool {
8946        while !self.core.output_pending().is_empty() {
8947            // The final GOAWAY takes the `close_notify` with it: one
8948            // `write_tls` instead of this one and `H2Shell::close`'s.
8949            let (size, status) = if self.core.output_flush_closes_connection() {
8950                let pending = self.core.output_pending().len();
8951                let (size, status) = self
8952                    .socket
8953                    .socket_write_then_close(self.core.output_pending());
8954                if size == pending {
8955                    self.core.close_notify_sent = true;
8956                }
8957                (size, status)
8958            } else {
8959                self.socket.socket_write(self.core.output_pending())
8960            };
8961            self.record_write(status);
8962            #[cfg(debug_assertions)]
8963            trace!(
8964                "{} flush_output_to_socket: written={}, status={:?}, wants_write={}",
8965                log_context!(self.core),
8966                size,
8967                status,
8968                self.tls_wants_write()
8969            );
8970            if self.core.consume_output_flush(size, status) {
8971                return true;
8972            }
8973        }
8974        false
8975    }
8976
8977    /// Directly flush the ordered output queue to the socket without going
8978    /// through the full writable() path. Used during shutdown when the event
8979    /// loop won't deliver new epoll events for this session (edge-triggered).
8980    ///
8981    /// Whatever is queued can go at any time: every entry is a whole frame
8982    /// (or the rest of one a stalled write cut), queued in wire order, and
8983    /// the queue shares nothing with the read side (#1604). A stream parked
8984    /// by a stalled write keeps its park; only its already-started frame
8985    /// lives in the queue.
8986    pub fn flush_output_buffer(&mut self) {
8987        if self.flush_output_to_socket() {
8988            return;
8989        }
8990        if self.tls_wants_write() {
8991            let (_size, status) = self.flush_tls_records();
8992            let _ = update_readiness_after_write(0, status, &mut self.core.readiness);
8993        }
8994    }
8995
8996    pub fn close<E, L>(&mut self, context: &mut Context<L>, mut endpoint: E)
8997    where
8998        E: Endpoint,
8999        L: ListenerHandler + L7ListenerHandler,
9000    {
9001        match self.core.position {
9002            Position::Client(_, _, BackendStatus::KeepAlive) => {
9003                error!(
9004                    "{} H2 connections do not use KeepAlive backend status",
9005                    log_context!(self.core)
9006                );
9007                return;
9008            }
9009            Position::Client(..) => {}
9010            Position::Server => {
9011                let tls_pending_before = self.tls_wants_write();
9012                if !self.core.stream_table.streams().is_empty()
9013                    || tls_pending_before
9014                    || self.core.stream_table.expect_write().is_some()
9015                {
9016                    debug!(
9017                        "{} H2 close with active state: state={:?}, streams={}, expect_write={:?}, wants_write={}, readiness={:?}",
9018                        log_context!(self.core),
9019                        self.core.state,
9020                        self.core.stream_table.streams().len(),
9021                        self.core.stream_table.expect_write(),
9022                        tls_pending_before,
9023                        self.core.readiness
9024                    );
9025                    for (stream_id, global_stream_id) in self.core.stream_table.streams() {
9026                        let stream = &context.streams[*global_stream_id];
9027                        debug!(
9028                            "{}   close stream id={} gid={}: state={:?}, front_eos={}, back_eos={}, front_phase={:?}, back_phase={:?}, front_completed={}, back_completed={}",
9029                            log_context!(self.core),
9030                            stream_id,
9031                            global_stream_id,
9032                            stream.state,
9033                            stream.front_received_end_of_stream,
9034                            stream.back_received_end_of_stream,
9035                            stream.front.parsing_phase,
9036                            stream.back.parsing_phase,
9037                            stream.front.is_completed(),
9038                            stream.back.is_completed()
9039                        );
9040                    }
9041                }
9042                if !self.core.close_notify_sent {
9043                    trace!("{} H2 SENDING CLOSE NOTIFY", log_context!(self.core));
9044                }
9045                let (tls_pending_after, drain_rounds) =
9046                    drain_tls_close_notify(&mut self.socket, &mut self.core.close_notify_sent);
9047                if tls_pending_after {
9048                    // Severity tiering: key on stream-count + close-state, not
9049                    // peer-vs-operator. Composes with the send-side `H2Error`
9050                    // variant tier in `goaway()` — both rules demote benign
9051                    // paths and keep loss-bearing paths loud.
9052                    //
9053                    // - `streams != 0`           -> `error!`: live streams at
9054                    //   close time, response-byte loss is possible.
9055                    // - `streams == 0` AND state in {GoAway, Error}
9056                    //                             -> `warn!`: idle close after
9057                    //   a GOAWAY exchange (peer-initiated abort or our own
9058                    //   graceful drain). What's stranded is best-effort
9059                    //   GOAWAY/close_notify; no application data was queued.
9060                    // - `streams == 0` from any other state
9061                    //                             -> `error!`: unexpected
9062                    //   teardown path (no GOAWAY exchange) — keep loud so
9063                    //   unknown failure modes surface.
9064                    if !self.core.stream_table.streams().is_empty() {
9065                        error!(
9066                            "{} TLS buffer NOT fully drained on close: \
9067                             pending_before={}, pending_after={}, drain_rounds={}, \
9068                             state={:?}, streams={}, expect_write={:?}, \
9069                             close_notify_sent={}, readiness={:?}",
9070                            log_context!(self.core),
9071                            tls_pending_before,
9072                            tls_pending_after,
9073                            drain_rounds,
9074                            self.core.state,
9075                            self.core.stream_table.streams().len(),
9076                            self.core.stream_table.expect_write(),
9077                            self.core.close_notify_sent,
9078                            self.core.readiness
9079                        );
9080                    } else if matches!(self.core.state, H2State::GoAway | H2State::Error) {
9081                        warn!(
9082                            "{} TLS buffer NOT fully drained on close: \
9083                             pending_before={}, pending_after={}, drain_rounds={}, \
9084                             state={:?}, streams={}, expect_write={:?}, \
9085                             close_notify_sent={}, readiness={:?}",
9086                            log_context!(self.core),
9087                            tls_pending_before,
9088                            tls_pending_after,
9089                            drain_rounds,
9090                            self.core.state,
9091                            self.core.stream_table.streams().len(),
9092                            self.core.stream_table.expect_write(),
9093                            self.core.close_notify_sent,
9094                            self.core.readiness
9095                        );
9096                    } else {
9097                        error!(
9098                            "{} TLS buffer NOT fully drained on close: \
9099                             pending_before={}, pending_after={}, drain_rounds={}, \
9100                             state={:?}, streams={}, expect_write={:?}, \
9101                             close_notify_sent={}, readiness={:?}",
9102                            log_context!(self.core),
9103                            tls_pending_before,
9104                            tls_pending_after,
9105                            drain_rounds,
9106                            self.core.state,
9107                            self.core.stream_table.streams().len(),
9108                            self.core.stream_table.expect_write(),
9109                            self.core.close_notify_sent,
9110                            self.core.readiness
9111                        );
9112                    }
9113                }
9114                return;
9115            }
9116        }
9117        // reconnection is handled by the server for each stream separately
9118        for global_stream_id in self.core.stream_table.streams().values() {
9119            trace!(
9120                "{} end stream: {}",
9121                log_context!(self.core),
9122                global_stream_id
9123            );
9124            if let StreamState::Linked(token) = context.streams[*global_stream_id].state {
9125                endpoint.end_stream(token, *global_stream_id, context);
9126            }
9127        }
9128    }
9129
9130    /// Wrap a freshly built core around the socket it will speak through.
9131    ///
9132    /// The `peer_addr()` call is here because this is the layer that has a
9133    /// socket to ask. It is still the one read of it on the connection's whole
9134    /// lifetime — `ConnectionH2::peer_address` documents why the answer is
9135    /// snapshotted rather than re-read — and it now happens one layer out,
9136    /// before the core exists rather than inside its constructor.
9137    #[allow(clippy::too_many_arguments)]
9138    pub fn new(
9139        session_ulid: Ulid,
9140        socket: Front,
9141        position: super::Position,
9142        buffers: &mut dyn super::buffer_source::BufferSource,
9143        flood_config: H2FloodConfig,
9144        connection_config: H2ConnectionConfig,
9145        stream_idle_timeout: std::time::Duration,
9146        graceful_shutdown_deadline: Option<std::time::Duration>,
9147        timeout_duration: std::time::Duration,
9148        expect_read: Option<(H2StreamId, usize)>,
9149        readiness_interest: sozu_command::ready::Ready,
9150    ) -> Option<Self> {
9151        let core = ConnectionH2::new(
9152            session_ulid,
9153            socket.peer_addr(),
9154            position,
9155            buffers,
9156            flood_config,
9157            connection_config,
9158            stream_idle_timeout,
9159            graceful_shutdown_deadline,
9160            timeout_duration,
9161            expect_read,
9162            readiness_interest,
9163        )?;
9164        Some(H2Shell {
9165            core,
9166            socket,
9167            io_slices: Vec::new(),
9168            local_rtt: Cell::new(None),
9169            write_failed: false,
9170        })
9171    }
9172}
9173
9174/// The socket shell of the H2 data path.
9175///
9176/// Every production `self.socket` in this file is in this block and its
9177/// sibling above it, and `ConnectionH2` has none. `H2Shell::readable` and
9178/// `H2Shell::write_streams` are the two byte movers: each is the caller that
9179/// sits between one of the core's two poll/handle pairs —
9180/// [`ConnectionH2::poll_read_target`] with [`ConnectionH2::handle_read`], and
9181/// [`ConnectionH2::poll_write_target`] with [`ConnectionH2::handle_write`].
9182/// All four are `pub`, and so are the buffers they name
9183/// ([`ConnectionH2::read_space`], [`ConnectionH2::write_buffer`]), so a driver
9184/// outside this crate can stand exactly where these two functions stand.
9185/// `sim/tests/h2_simulation.rs` is that driver, and it is what makes the claim
9186/// measurable rather than asserted.
9187///
9188/// `H2Shell::writable` is the third member, and the only socket it touches it
9189/// touches through the three TLS seams — `H2Shell::tls_wants_write`,
9190/// `H2Shell::flush_tls_records` and `H2Shell::begin_tls_close` — whose bodies
9191/// live here too. It drives two more poll/handle pairs:
9192/// [`ConnectionH2::flush_pending_control_frames`] with
9193/// [`ConnectionH2::handle_control_flush`], and
9194/// [`ConnectionH2::dispatch_writable_state`] with
9195/// [`ConnectionH2::dispatch_writable_state_after_flush`]. Neither takes a
9196/// `Front`: the `(H2State, Position)` dispatch and the control-frame walk are
9197/// both drivable without a socket, which is the whole claim.
9198///
9199/// The fourth pair is `ConnectionH2::force_disconnect` with
9200/// [`ConnectionH2::force_disconnect_after_query`], and it is the one that
9201/// cannot ride a return value — see [`H2ForceDisconnectTarget`]. `H2Shell::settled`
9202/// is where it lands, and every `MuxResult` a core step hands this block goes
9203/// through it.
9204///
9205/// # Why `write_streams` stays private, and where the `unsafe` windows are
9206///
9207/// `H2Shell::write_streams` drives the `Vec<IoSlice<'static>>` that brackets
9208/// `h2_transmit::gather` and `h2_transmit::confirm`, and it drives it because
9209/// the loop that opens and closes that bracket lives with it. The vector
9210/// itself is the shell's private `io_slices` field, so its capacity outlives
9211/// the pass while no descriptor does. There are TWO
9212/// `unsafe` blocks on this path and they nest:
9213///
9214/// - the INNER one is `gather`'s `slice::from_raw_parts`, which re-labels
9215///   borrows of `kawa.storage` as `'static`. It opens and closes inside that
9216///   function, exactly as it always did.
9217/// - the OUTER one is the `unsafe { … }` around the `gather` CALL below, which
9218///   exists because `gather` is `pub unsafe fn`: the lifetime obligation is
9219///   stated in its signature and discharged by the caller. It wraps the call
9220///   and nothing else — not the vectored write, not the `confirm`. It is the
9221///   only `unsafe` block in production code in this file.
9222///
9223/// **The window did not widen when `write_streams` moved onto `H2Shell`, nor
9224/// when the vector moved into a field of it.** It still opens at that `gather`
9225/// call and closes at the `confirm` three statements later, where
9226/// `io_slices.clear()` runs before the consume which may relocate the storage.
9227/// The only thing entered while the descriptors are live is
9228/// `SocketHandler::socket_write_vectored`, which reborrows them for the
9229/// duration of the call and cannot retain them. No `IoSlice<'static>`
9230/// survives one iteration of the loop: what the field keeps between passes is
9231/// capacity, and the vector is empty whenever `write_streams` is not inside
9232/// that bracket. `gather` clears it on entry as well, so even a descriptor
9233/// stranded by a panic out of the vectored write is discarded before anything
9234/// could read it. What changed is the
9235/// receiver, from `&mut ConnectionH2<Front>` to `&mut H2Shell<Front>`, and the
9236/// buffer now arrives through [`ConnectionH2::write_buffer`] instead of the
9237/// free function — neither of which moves either end of the bracket.
9238///
9239/// That the bracket does not span the poll/handle seam matters MORE now, not
9240/// less: the seam is `pub`, the buffers it names are `pub`, and an
9241/// out-of-crate driver really does exist, so halves on opposite sides of it
9242/// could be separated by a caller this crate cannot see. `write_streams` stays
9243/// private for that reason, on `H2Shell` exactly as it was on `ConnectionH2`.
9244/// `h2_transmit`'s pair is exported — `gather` as `pub unsafe fn`, `confirm`
9245/// as a plain `pub fn` that produces no descriptor and so leaves no
9246/// obligation — which moves the same argument onto that driver's own bracket
9247/// rather than dissolving it. `H2Harness::write_streams` in
9248/// `sim/tests/h2_simulation.rs` opens and closes its own bracket inside its
9249/// own single function, for the same reason and with the same shape.
9250impl<Front: SocketHandler> H2Shell<Front> {
9251    /// Answer a core step, settling any [`H2ForceDisconnectTarget`] it parked
9252    /// on the way.
9253    ///
9254    /// The one place the socket answers a force-disconnect, and the reason
9255    /// `ConnectionH2::force_disconnect` can be raised from seventeen sites
9256    /// that hold no socket. Every `MuxResult` a core step hands this impl
9257    /// block leaves through here, which is what makes the discipline
9258    /// greppable rather than asserted: a core call in this block whose type is
9259    /// [`MuxResult`] and which does not appear as this function's argument is
9260    /// a missed settlement.
9261    ///
9262    /// `result` is the core's provisional answer and survives untouched when
9263    /// nothing was parked — the overwhelmingly common case, since only a
9264    /// force-disconnect parks anything.
9265    /// [`ConnectionH2::force_disconnect`], settled.
9266    ///
9267    /// The public spelling of the decision: the core raises it and this reads
9268    /// the query it could not. Every out-of-shell caller goes through here.
9269    pub fn force_disconnect(&mut self) -> MuxResult {
9270        let result = self.core.force_disconnect();
9271        self.settled(result)
9272    }
9273
9274    /// [`ConnectionH2::graceful_goaway`], settled.
9275    ///
9276    /// Reaches `ConnectionH2::force_disconnect` through
9277    /// `ConnectionH2::send_initial_goaway`'s serialisation-failure arm.
9278    pub fn graceful_goaway(&mut self, now: Instant) -> MuxResult {
9279        let result = self.core.graceful_goaway(now);
9280        self.settled(result)
9281    }
9282
9283    /// [`ConnectionH2::goaway_before_forced_close`], settled.
9284    ///
9285    /// Reaches `ConnectionH2::force_disconnect` through the
9286    /// serialisation-failure arm of the final GOAWAY.
9287    pub fn goaway_before_forced_close<L>(&mut self, context: &Context<L>) -> MuxResult
9288    where
9289        L: ListenerHandler + L7ListenerHandler,
9290    {
9291        let result = self.core.goaway_before_forced_close(context);
9292        self.settled(result)
9293    }
9294
9295    /// [`ConnectionH2::start_stream`], settled.
9296    ///
9297    /// Answers the core's `bool` unchanged. The settlement is still required
9298    /// and still belongs HERE: the core reaches
9299    /// `ConnectionH2::graceful_goaway` from its stream-id-exhaustion arm, and
9300    /// the readiness that settlement writes is read by `Mux` before it decides
9301    /// whether this connection gets a `writable` at all.
9302    pub fn start_stream<L>(&mut self, stream: GlobalStreamId, context: &mut Context<L>) -> bool
9303    where
9304        L: ListenerHandler + L7ListenerHandler,
9305    {
9306        let started = self.core.start_stream(stream, context);
9307        self.settle();
9308        started
9309    }
9310
9311    /// [`ConnectionH2::cancel_timed_out_streams`], settled.
9312    ///
9313    /// The core answers nothing and already discards the flood-violation
9314    /// `MuxResult` its `ConnectionH2::enqueue_rst` sweep can raise — see its
9315    /// own comment. That discard is preserved: what this adds is the readiness
9316    /// half of a force-disconnect that discard used to leave completed.
9317    pub fn cancel_timed_out_streams<E, L>(&mut self, context: &mut Context<L>, endpoint: &mut E)
9318    where
9319        E: Endpoint,
9320        L: ListenerHandler + L7ListenerHandler,
9321    {
9322        self.core.cancel_timed_out_streams(
9323            context,
9324            &mut ShellEndpoint::new(endpoint, &self.socket, &self.local_rtt),
9325        );
9326        self.settle();
9327    }
9328
9329    /// [`ConnectionH2::end_stream`], forwarded and drained.
9330    ///
9331    /// No settlement: `ConnectionH2::end_stream` cannot reach
9332    /// `ConnectionH2::force_disconnect`. It is here only because
9333    /// `Connection`'s `forward!` needs one name on both arms.
9334    ///
9335    /// It still has to drain. The core's `end_stream` serialises a
9336    /// RST_STREAM(CANCEL) for a stream the peer abandoned, and that is the one
9337    /// producer this file reaches through a public entry point that settles
9338    /// neither through `H2Shell::settled` nor through `H2Shell::settle`.
9339    pub fn end_stream<L>(&mut self, stream_gid: GlobalStreamId, context: &mut Context<L>)
9340    where
9341        L: ListenerHandler + L7ListenerHandler,
9342    {
9343        self.core.end_stream(stream_gid, context);
9344        self.record_pending_metrics();
9345    }
9346
9347    /// Drive [`ConnectionH2::flush_pending_control_frames`] to a terminal
9348    /// answer, performing the output flush it asks for.
9349    ///
9350    /// The loop is the shell's because the socket is: the core walks its
9351    /// stages, queues their frames behind the ordered output, and stops once
9352    /// with the bytes it cannot move, then is told what the write reported.
9353    /// At most two iterations run, and a stall leaves the loop with
9354    /// [`H2ControlFlushTarget::Stalled`], which is the caller's cue to read
9355    /// the TLS query the stalled write just changed the answer to.
9356    fn drive_control_flush(&mut self) -> ControlFlushOutcome {
9357        let mut target = self.core.flush_pending_control_frames();
9358        loop {
9359            match target {
9360                H2ControlFlushTarget::FlushOutput => {
9361                    let stalled = self.flush_output_to_socket();
9362                    target = self.core.handle_control_flush(stalled);
9363                }
9364                H2ControlFlushTarget::Proceed => return ControlFlushOutcome::Proceed,
9365                H2ControlFlushTarget::Done(result) => return ControlFlushOutcome::Done(result),
9366                H2ControlFlushTarget::Stalled => return ControlFlushOutcome::Stalled,
9367            }
9368        }
9369    }
9370
9371    fn settled(&mut self, result: MuxResult) -> MuxResult {
9372        let settled = match self.core.poll_force_disconnect() {
9373            None => result,
9374            Some(target) => {
9375                let tls_wants_write = self.tls_wants_write();
9376                self.core
9377                    .force_disconnect_after_query(target, tls_wants_write)
9378            }
9379        };
9380        self.record_pending_metrics();
9381        settled
9382    }
9383
9384    /// Record every [`MetricEvent`] the core queued during the step that just
9385    /// ended.
9386    ///
9387    /// The shell half of the boundary: the core states the arithmetic, this
9388    /// writes it into the worker-local `METRICS` aggregator.
9389    ///
9390    /// Three sites, and between them they cover every producer:
9391    ///
9392    /// - [`Self::settled`], so after every `readable`, `writable`,
9393    ///   `start_stream`, `cancel_timed_out_streams`, `force_disconnect` and
9394    ///   `graceful_goaway`, and after `writable_inner`'s
9395    ///   [`Self::drive_control_flush`], whose only caller it is;
9396    /// - [`Self::end_stream`], the one public entry point that reaches a
9397    ///   producer and settles through neither;
9398    /// - [`Drop`].
9399    ///
9400    /// The remaining public entry points — `close`, `initiate_close_notify`,
9401    /// `flush_output_buffer`, `has_pending_write`, `has_pending_write_full` —
9402    /// reach no producer but one: they touch the TLS layer, the output queue
9403    /// and readiness, and of the core methods they call
9404    /// (`consume_output_flush`, `ensure_tls_flushed`, `has_pending_write_with`,
9405    /// `output_pending`) only `consume_output_flush` queues a metric, the
9406    /// overhead byte count, which the next [`Self::settled`] records.
9407    ///
9408    /// A step that reached none of the three would still not lose an event:
9409    /// the queue is FIFO, so the next drain records the same values, and
9410    /// `Drop` drains whatever is left. That is what bounds the queue, and what
9411    /// keeps "drain here" a scheduling choice rather than a correctness one.
9412    fn record_pending_metrics(&mut self) {
9413        for event in self.core.drain_metric_events() {
9414            record_metric(event);
9415        }
9416    }
9417
9418    /// [`ConnectionH2::settled`] for a core step whose own answer is not a
9419    /// [`MuxResult`], or is one this layer has always dropped.
9420    ///
9421    /// `ConnectionH2::start_stream` answers a `bool` and
9422    /// `ConnectionH2::cancel_timed_out_streams` answers nothing, yet both
9423    /// reach `ConnectionH2::force_disconnect` — the first through
9424    /// `ConnectionH2::graceful_goaway`, the second through
9425    /// `ConnectionH2::enqueue_rst`'s flood escalation, which it already
9426    /// discards with `let _ =`. The settlement still has to happen HERE rather
9427    /// than at the next entry point, because it writes `readiness` and `Mux`
9428    /// reads `Readiness::filter_interest` between entry points to decide
9429    /// whether to call `readable`/`writable` at all.
9430    fn settle(&mut self) {
9431        let _ = self.settled(MuxResult::Continue);
9432    }
9433
9434    /// Drive one frontend read pass.
9435    ///
9436    /// The core lives in `Self::poll_read_target` and `Self::handle_read`,
9437    /// both `pub`; this function is the caller that sits between them, and its
9438    /// `self.socket.socket_read` is the only socket touch on the whole H2 read
9439    /// path. It lives in the shell impl rather than among the core impls for
9440    /// the reason stated there: the socket touch sits where the socket does,
9441    /// and the frame state machine between the two calls is drivable without
9442    /// one, exactly as `h2_transmit::gather`/`confirm` already bracket the
9443    /// vectored write.
9444    pub fn readable<E, L>(&mut self, context: &mut Context<L>, endpoint: E) -> MuxResult
9445    where
9446        E: Endpoint,
9447        L: ListenerHandler + L7ListenerHandler,
9448    {
9449        if self.core.is_lingering() {
9450            // Entry point: adopt the mux's snapshot, as `poll_read_target`
9451            // would. `graceful_shutdown_deadline_elapsed` reads it, and a
9452            // snapshot frozen at the linger's start would keep a soft stop's
9453            // budget from ever elapsing.
9454            self.core.adopt_now(context.now);
9455            return self.drain_linger(context.now);
9456        }
9457        let result = self.readable_inner(context, endpoint);
9458        self.settled(result)
9459    }
9460
9461    /// Turn a close decided by a writable pass that began after the final
9462    /// GOAWAY ([`ConnectionH2::lingers_instead_of_closing`]) into a lingering
9463    /// close, unless the client hung up meanwhile: queue and
9464    /// flush `close_notify` first (a TLS alert must precede the FIN), then
9465    /// shut the write side down, so the FIN follows everything sent, and
9466    /// read what the client still sends until [`Self::drain_linger`] closes.
9467    /// Any other result is returned unchanged.
9468    fn linger_instead_of_closing(&mut self, result: MuxResult) -> MuxResult {
9469        if !matches!(result, MuxResult::CloseSession) || self.core.frontend_hung_up_while_draining()
9470        {
9471            return result;
9472        }
9473        let (duration, max_bytes) = self.core.linger_budget();
9474        let deadline = match self.core.linger {
9475            Linger::Pending { deadline } => deadline,
9476            _ => match self.core.now.checked_add(duration) {
9477                Some(deadline) => deadline,
9478                None => return MuxResult::CloseSession,
9479            },
9480        };
9481        if self.initiate_close_notify() {
9482            // `close_notify` is pending: the next writable pass flushes it
9483            // and closes again, which lands here once nothing is left. The
9484            // state is `Error` by then, so the pending linger is what keeps
9485            // that pass eligible.
9486            self.core.linger = Linger::Pending { deadline };
9487            return MuxResult::Continue;
9488        }
9489        if let Err(e) = super::shutdown_write(self.socket.socket_ref(), false) {
9490            debug!(
9491                "{} H2 closing without lingering after the final GOAWAY: shutdown failed: {:?}",
9492                log_context!(self.core),
9493                e
9494            );
9495            return MuxResult::CloseSession;
9496        }
9497        debug!(
9498            "{} H2 lingering after the final GOAWAY: draining what the client still sends",
9499            log_context!(self.core)
9500        );
9501        self.core.linger = Linger::Draining {
9502            deadline,
9503            remaining: max_bytes,
9504        };
9505        // Fixed, never re-armed: the deadline bounds the whole drain.
9506        self.core.timeout_deadline = Some(deadline);
9507        self.core.readiness.interest = Ready::READABLE | Ready::HUP | Ready::ERROR;
9508        self.core.readiness.event.remove(Ready::WRITABLE);
9509        // Edge-triggered epoll will not report bytes already queued.
9510        self.core.readiness.event.insert(Ready::READABLE);
9511        MuxResult::Continue
9512    }
9513
9514    /// Read and drop what the client still sends while lingering. Closes on
9515    /// the client's EOF, a socket error, the byte budget, or the deadline;
9516    /// otherwise waits for the next READABLE.
9517    fn drain_linger(&mut self, now: Instant) -> MuxResult {
9518        let Linger::Draining {
9519            deadline,
9520            mut remaining,
9521        } = self.core.linger
9522        else {
9523            unreachable!("drain_linger runs only while lingering");
9524        };
9525        if now >= deadline {
9526            debug!(
9527                "{} H2 lingering close: deadline reached",
9528                log_context!(self.core)
9529            );
9530            return MuxResult::CloseSession;
9531        }
9532        let result = match drain_discard(self.socket.socket_ref(), &mut remaining, |size| {
9533            record_metric(MetricEvent::FrontendBytesIn(size as i64));
9534        }) {
9535            LingerRead::Wait => {
9536                self.core.readiness.event.remove(Ready::READABLE);
9537                MuxResult::Continue
9538            }
9539            LingerRead::Close => {
9540                debug!(
9541                    "{} H2 lingering close: EOF, error or {} bytes left of the budget",
9542                    log_context!(self.core),
9543                    remaining
9544                );
9545                MuxResult::CloseSession
9546            }
9547        };
9548        self.core.linger = Linger::Draining {
9549            deadline,
9550            remaining,
9551        };
9552        result
9553    }
9554
9555    fn readable_inner<E, L>(&mut self, context: &mut Context<L>, mut endpoint: E) -> MuxResult
9556    where
9557        E: Endpoint,
9558        L: ListenerHandler + L7ListenerHandler,
9559    {
9560        match self.core.poll_read_target(
9561            context,
9562            &mut ShellEndpoint::new(&mut endpoint, &self.socket, &self.local_rtt),
9563        ) {
9564            H2ReadTarget::Done(result) => result,
9565            H2ReadTarget::Skip(stream_id) => self.core.handle_read(
9566                context,
9567                ShellEndpoint::new(&mut endpoint, &self.socket, &self.local_rtt),
9568                stream_id,
9569                H2ReadOutcome::Skipped,
9570            ),
9571            H2ReadTarget::Fill { stream_id, amount } => {
9572                let space = self.core.read_space(context, stream_id, amount);
9573                let (size, status) = self.socket.socket_read(space);
9574                self.core.handle_read(
9575                    context,
9576                    ShellEndpoint::new(&mut endpoint, &self.socket, &self.local_rtt),
9577                    stream_id,
9578                    H2ReadOutcome::Filled {
9579                        amount,
9580                        size,
9581                        status,
9582                    },
9583                )
9584            }
9585        }
9586    }
9587
9588    /// Drive one frontend write pass.
9589    ///
9590    /// The core lives in `Self::flush_pending_control_frames`,
9591    /// `Self::dispatch_writable_state` and
9592    /// `Self::dispatch_writable_state_after_flush`; this function is the
9593    /// caller that sits between them, and every TLS seam call this function's
9594    /// own body used to make inline is here. It lives in the shell impl rather
9595    /// than among the core impls for the reason stated there: the socket
9596    /// touches sit where the socket does, and the `(H2State, Position)`
9597    /// dispatch between them is drivable without one.
9598    ///
9599    /// The pass performs the query / flush / query triple `h2_close` names
9600    /// TWICE, and the second time only for one arm:
9601    ///
9602    /// 1. the preamble — read `Self::tls_wants_write`, flush if it says yes.
9603    ///    It pushes bytes for every state, which is why it is unconditional
9604    ///    rather than owned by a close arm.
9605    /// 2. a FRESH read of the same query, which is that preamble's post-flush
9606    ///    answer and the pre-flush question both close arms decide on. It is
9607    ///    handed to `Self::dispatch_writable_state` as an input.
9608    /// 3. for `H2WritableStateTarget::Flush` alone — the `H2State::GoAway`
9609    ///    arm — `Self::flush_tls_records` again, a third read, and
9610    ///    `Self::dispatch_writable_state_after_flush`.
9611    ///
9612    /// Each read is a separate `let`, and the third shadows the second inside
9613    /// the `Flush` arm only. That is the whole guard against the confusion
9614    /// `h2_close::TlsFlushPhase` exists to prevent: "does rustls hold records"
9615    /// and "did the kernel take them" are two questions, the second cannot be
9616    /// read off the first, and a core that reused one answer for both would
9617    /// close a connection with records pending.
9618    ///
9619    /// The tick count is load-bearing: the flush and the re-query happen
9620    /// within THIS call and the pass does not return `Continue` to be called
9621    /// again, which `a_flush_that_succeeds_closes_within_one_writable_call`
9622    /// pins. See `h2_close`'s module doc for why.
9623    pub fn writable<E, L>(&mut self, context: &mut Context<L>, endpoint: E) -> MuxResult
9624    where
9625        E: Endpoint,
9626        L: ListenerHandler + L7ListenerHandler,
9627    {
9628        if self.core.is_lingering() {
9629            // Entry point: adopt the mux's snapshot, as `writable_inner` does.
9630            self.core.adopt_now(context.now);
9631            // The write side is shut down: nothing is left to write.
9632            self.core.readiness.interest.remove(Ready::WRITABLE);
9633            self.core.readiness.event.remove(Ready::WRITABLE);
9634            return MuxResult::Continue;
9635        }
9636        // Asked before the pass: the close it decides moves the state from
9637        // `GoAway` to `Error` (`ConnectionH2::force_disconnect`).
9638        let after_final_goaway = self.core.lingers_instead_of_closing();
9639        let result = self.writable_inner(context, endpoint);
9640        if self.write_failed {
9641            // The client is gone or its socket can never be flushed: close
9642            // at once, as an H1 frontend does (`ConnectionH1::writable`).
9643            debug!(
9644                "{} H2 closing the frontend after a failed write",
9645                log_context!(self.core)
9646            );
9647            return MuxResult::CloseSession;
9648        }
9649        let result = self.settled(result);
9650        if after_final_goaway {
9651            self.linger_instead_of_closing(result)
9652        } else {
9653            result
9654        }
9655    }
9656
9657    fn writable_inner<E, L>(&mut self, context: &mut Context<L>, endpoint: E) -> MuxResult
9658    where
9659        E: Endpoint,
9660        L: ListenerHandler + L7ListenerHandler,
9661    {
9662        // Entry point: adopt the mux's snapshot for this pass.
9663        self.core.adopt_now(context.now);
9664        // Only this pass's writes may withhold its TLS re-arm.
9665        self.core.kernel_refused_write = false;
9666        self.core.prune_inactive_streams_while_closing(context);
9667
9668        match self.drive_control_flush() {
9669            ControlFlushOutcome::Done(result) => return result,
9670            // The stalled-drain tail the stages used to run inline. The
9671            // query belongs here and not there: `flush_output_to_socket` is what
9672            // changed the answer, so it has to be read after the stall, and
9673            // this is the layer that owns the socket.
9674            ControlFlushOutcome::Stalled => {
9675                let tls_wants_write = self.tls_wants_write();
9676                self.core.ensure_tls_flushed(tls_wants_write);
9677                return MuxResult::Continue;
9678            }
9679            ControlFlushOutcome::Proceed => {}
9680        }
9681
9682        // Flush any pending TLS records before state-specific processing.
9683        // This ensures response DATA frames that were accepted by rustls
9684        // (via socket_write_vectored in write_streams) are pushed to the
9685        // TCP socket even when the connection is in GoAway or Error state.
9686        // Without this, the state-specific handlers may call force_disconnect()
9687        // before the response data reaches the kernel's TCP send buffer.
9688        if self.tls_wants_write() {
9689            self.flush_tls_records();
9690        }
9691
9692        // A fresh query, not the binding above: this one asks whether the
9693        // flush between them landed. It is read here rather than inside the
9694        // two close arms that consult it because those arms no longer touch
9695        // the socket, and it is read ONCE because only one of them can run.
9696        let tls_wants_write = self.tls_wants_write();
9697        match self.core.dispatch_writable_state(tls_wants_write) {
9698            H2WritableStateTarget::Done(result) => result,
9699            // The `H2State::GoAway` arm's own flush, the second this pass
9700            // attempts, and the only *action* the close decision cannot take
9701            // for itself.
9702            H2WritableStateTarget::Flush => {
9703                self.flush_tls_records();
9704                // Again a fresh query, and again a different question from
9705                // the one that produced `Flush`: `flush_tls_records`'s
9706                // `(size, status)` is discarded, so this read is the only way
9707                // the pass learns whether the records reached the kernel.
9708                let tls_wants_write = self.tls_wants_write();
9709                self.core
9710                    .dispatch_writable_state_after_flush(tls_wants_write)
9711            }
9712            H2WritableStateTarget::WriteStreams => self.write_streams(context, endpoint),
9713        }
9714    }
9715
9716    /// Write application data (request/response bodies, headers) across all
9717    /// active streams, respecting priority ordering and flow control.
9718    ///
9719    /// This is the main data-plane write path: it resumes any partially-written
9720    /// stream, prepares new frames via the H2 block converter, flushes them to
9721    /// the socket, and recycles completed streams.
9722    ///
9723    /// The core lives in [`ConnectionH2::poll_write_target`] and [`ConnectionH2::handle_write`],
9724    /// both `pub`; this function is the caller that sits between them, and its
9725    /// `self.socket.socket_write_vectored` is the only place a stream's bytes
9726    /// reach the socket. It lives in the shell impl rather than among the core
9727    /// impls for the reason stated there, exactly as `readable()` keeps
9728    /// `socket_read` outside [`ConnectionH2::poll_read_target`].
9729    ///
9730    /// The pass's last step is a socket touch too, and for the same reason it
9731    /// sits here: [`H2WriteTarget::Finalize`] is answered by reading
9732    /// [`Self::tls_wants_write`], driving the [`H2FinalizeTarget`]
9733    /// [`ConnectionH2::finalize_write`] answers — performing
9734    /// [`Self::flush_tls_records`] for [`H2FinalizeTarget::Flush`] and not for
9735    /// [`H2FinalizeTarget::SkipFlush`] — and reading the query again for
9736    /// [`ConnectionH2::finalize_write_after_flush`]. That is the query / flush / query
9737    /// triple `h2_close::TlsFlushPhase` names, and the three TLS seams it
9738    /// reaches are what a byte-in / byte-out core receives as inputs instead
9739    /// of asking for.
9740    ///
9741    /// Unlike `readable()` this is a LOOP and not a match: one read pass
9742    /// performs one `socket_read`, while one write pass performs an unbounded
9743    /// number of vectored writes — the pre-image's `while !kawa.out.is_empty()`
9744    /// nested inside its walk of the scheduler's order. Every iteration here is
9745    /// one of those rounds.
9746    ///
9747    /// The `Vec<IoSlice<'static>>` came here WITH the loop, and is only ever
9748    /// filled inside this one private function, on purpose. It is the shell's
9749    /// private `io_slices` field rather than a local so that a transmitting
9750    /// pass reuses its capacity instead of allocating it: the local cost one
9751    /// heap allocation per pass, at least one per response.
9752    /// [`h2_transmit::gather`] is a `pub unsafe fn` that hands back
9753    /// descriptors carrying a lifetime they do
9754    /// not have, and [`h2_transmit::confirm`] must discharge them before the
9755    /// consume that may relocate `kawa.storage`; bracketing the two around the
9756    /// write in three adjacent statements keeps that window exactly as wide as
9757    /// the pre-image's loop body kept it, and stops it from spanning the
9758    /// poll/handle boundary — now a `pub` one, which is why the bracket's
9759    /// locality is load-bearing rather than tidy. The `unsafe { … }` below
9760    /// wraps the `gather` call alone; see the shell impl's own note.
9761    ///
9762    /// The [`converter::H2BlockConverter`] is scoped to a single
9763    /// `kawa.prepare` call rather than to the whole per-stream loop, so the
9764    /// `&mut self.core.hpack` borrow it takes for the connection's HPACK encoder
9765    /// never spans the loop. Every `&self` / `&mut self` method is therefore
9766    /// callable inside the loop body; what remains deferred to after it —
9767    /// RST accounting and stream retirement — is deferred for its own
9768    /// ordering reasons, documented at each site.
9769    /// [`converter::H2ConverterPass`] carries the scratch buffers and the
9770    /// RFC 7541 §6.3 size-update signal from one stream's `prepare` to the
9771    /// next by moving them, so the narrower scope costs no copy.
9772    fn write_streams<E, L>(&mut self, context: &mut Context<L>, mut endpoint: E) -> MuxResult
9773    where
9774        E: Endpoint,
9775        L: ListenerHandler + L7ListenerHandler,
9776    {
9777        // No `arm_timeout()` here. `writable()` reaches this function on every
9778        // proxying-state pass, including one whose only output was the PING or
9779        // SETTINGS acknowledgement `flush_pending_control_frames` drained a few
9780        // statements earlier, so arming at pass entry renewed the session for a
9781        // peer that had sent nothing but keepalives. The write side's share of
9782        // LIFECYCLE §9 invariant 9 now lives in `ConnectionH2::handle_write`,
9783        // where the pass knows a real stream's bytes reached the socket.
9784        // Pre-compute byte totals for proportional overhead distribution.
9785        let mut pass = H2WritePass::new(self.core.compute_stream_byte_totals(context));
9786
9787        loop {
9788            match self.core.poll_write_target(
9789                context,
9790                &mut ShellEndpoint::new(&mut endpoint, &self.socket, &self.local_rtt),
9791                &mut pass,
9792            ) {
9793                H2WriteTarget::Done(result) => return result,
9794                H2WriteTarget::Finalize {
9795                    socket_write,
9796                    bytes_written,
9797                } => {
9798                    let tls_wants_write = self.tls_wants_write();
9799                    match self.core.finalize_write(
9800                        tls_wants_write,
9801                        socket_write,
9802                        bytes_written,
9803                        context,
9804                    ) {
9805                        H2FinalizeTarget::Done(result) => return result,
9806                        // The only *action* of the three TLS seams, and the
9807                        // one step the core cannot take for itself.
9808                        H2FinalizeTarget::Flush => {
9809                            self.flush_tls_records();
9810                        }
9811                        // The vectored writes above already attempted this
9812                        // pass's flush; a second empty-buffer write would be a
9813                        // syscall for nothing.
9814                        H2FinalizeTarget::SkipFlush => {}
9815                    }
9816                    // A fresh query, not the binding above: this one asks
9817                    // whether the step between them landed.
9818                    let tls_wants_write = self.tls_wants_write();
9819                    return self.core.finalize_write_after_flush(tls_wants_write);
9820                }
9821                H2WriteTarget::Transmit { stream_id } => {
9822                    // Gather / write / measure / confirm. The gather borrows
9823                    // the queued output and `kawa.storage`, and hands back
9824                    // descriptors with an extended lifetime; `confirm`
9825                    // discharges that obligation before the consume.
9826                    //
9827                    // SAFETY: neither the output queue nor `kawa` is dropped
9828                    // or mutated between this call and the `confirm` below,
9829                    // and that `confirm` clears `self.io_slices` before the
9830                    // `Kawa::consume` which may relocate `kawa.storage`. The
9831                    // only things entered while the descriptors are live are
9832                    // the vectored write, which reborrows them for the
9833                    // duration of the call and cannot retain them, and
9834                    // `frame_tail`, which only reads them.
9835                    let (prefix, offered) = unsafe {
9836                        self.core
9837                            .gather_transmit(context, stream_id, &mut self.io_slices)
9838                    };
9839                    let (size, status) = self.socket.socket_write_vectored(&self.io_slices);
9840                    self.record_write(status);
9841                    debug_assert!(
9842                        size <= offered,
9843                        "the socket reported {size} bytes written for an offer of {offered}"
9844                    );
9845                    let stream_size = size.saturating_sub(prefix);
9846                    // A write that took everything cut nothing: the walk is
9847                    // only for a partial write.
9848                    let tail = if size < offered {
9849                        h2_transmit::frame_tail(
9850                            &self.io_slices[usize::from(prefix > 0)..],
9851                            stream_size,
9852                        )
9853                    } else {
9854                        0
9855                    };
9856                    let kawa = self.core.write_buffer(context, stream_id);
9857                    h2_transmit::confirm(kawa, &mut self.io_slices, stream_size);
9858                    self.core.handle_write(
9859                        context,
9860                        stream_id,
9861                        H2Written {
9862                            size,
9863                            from_output: size - stream_size,
9864                            tail,
9865                        },
9866                        status,
9867                        &mut pass,
9868                    );
9869                }
9870            }
9871        }
9872    }
9873}
9874
9875#[cfg(test)]
9876pub(super) mod tests {
9877    use std::{cell::RefCell, collections::HashMap, rc::Rc};
9878
9879    use super::*;
9880    use crate::{
9881        pool::Pool,
9882        protocol::{
9883            kawa_h1::editor::HttpContext,
9884            mux::{
9885                BackendChange, BackendDelta, Connection,
9886                buffer_source::PoolBufferSource,
9887                connection::EndpointClient,
9888                router::Router,
9889                test_support::{TestListener, connected_socket, test_context},
9890            },
9891        },
9892        socket::SessionTcpStream,
9893    };
9894
9895    // ── H2FloodDetector / H2FloodViolation ─────────────────────────────
9896    //
9897    // H2FloodDetector's own unit tests (threshold trips, half-decay,
9898    // lifetime ceilings, metric-key uniqueness) moved to
9899    // `h2_flood_detector.rs`'s `#[cfg(test)] mod tests` alongside the type —
9900    // its fields are private to that module now, matching `HpackState`,
9901    // `H2FlowControl` and `H2StreamTable`. The old test comparing
9902    // default-constructed vs. explicitly-constructed detectors did not
9903    // move: it compared `H2FloodDetector::default()` against
9904    // `H2FloodDetector::new(...)`, and this extraction removed `impl Default
9905    // for H2FloodDetector` entirely (LIFECYCLE.md invariant 20's last
9906    // self-sampling-clock exception), so there is no second constructor left
9907    // for it to compare against.
9908
9909    // ── Prioriser / scheduler ───────────────────────────────────────────
9910    //
9911    // `Prioriser`'s own unit tests (defaults, the RFC 9218 urgency clamp,
9912    // the `MAX_PRIORITIES` cap, the standalone-PRIORITY acceptance filter,
9913    // the same-urgency incremental rotation) moved to `h2_scheduler.rs`'s
9914    // `#[cfg(test)] mod tests` alongside the type — its fields are private
9915    // to that module now, matching `HpackState`, `H2FlowControl`,
9916    // `H2StreamTable` and `H2FloodDetector`. The pass-scoped scheduling
9917    // tests that used to re-implement `ready_incremental_by_urgency`'s
9918    // arithmetic inline in the test body moved there too, and now drive
9919    // `ReadyIncrementalCensus`'s production methods instead of a copy of
9920    // them.
9921
9922    // ── H2FlowControl ───────────────────────────────────────────────────
9923    //
9924    // H2FlowControl's own unit tests (initial state, coalescing, saturation,
9925    // negative window, the ordering-determinism regression test) moved to
9926    // `h2_flow_control.rs`'s `#[cfg(test)] mod tests` alongside the type —
9927    // its fields are private to that module now, matching `HpackState`.
9928
9929    // ── H2FloodConfig ───────────────────────────────────────────────────
9930    //
9931    // Moved to `h2_flood_detector.rs`'s `#[cfg(test)] mod tests` alongside
9932    // `H2FloodConfig`, `H2FloodViolation` and `H2FloodDetector`.
9933
9934    // ── distribute_overhead ─────────────────────────────────────────────
9935
9936    #[test]
9937    fn test_distribute_overhead_proportional() {
9938        let mut metrics = SessionMetrics::new(None);
9939        let mut overhead_bin = 1000;
9940        let mut overhead_bout = 500;
9941
9942        // Stream transferred 60% of total bytes (not last stream)
9943        distribute_overhead(
9944            &mut metrics,
9945            &mut overhead_bin,
9946            &mut overhead_bout,
9947            (600, 300),  // stream_bytes
9948            (1000, 500), // total_bytes
9949            2,           // active_streams
9950            false,       // is_last_stream
9951        );
9952
9953        assert_eq!(metrics.bin, 600); // 60% of 1000
9954        assert_eq!(metrics.bout, 300); // 60% of 500
9955        assert_eq!(overhead_bin, 400); // 1000 - 600
9956        assert_eq!(overhead_bout, 200); // 500 - 300
9957    }
9958
9959    #[test]
9960    fn test_distribute_overhead_even_split_when_no_bytes() {
9961        let mut metrics = SessionMetrics::new(None);
9962        let mut overhead_bin = 100;
9963        let mut overhead_bout = 200;
9964
9965        // No bytes transferred -> even distribution (not last stream)
9966        distribute_overhead(
9967            &mut metrics,
9968            &mut overhead_bin,
9969            &mut overhead_bout,
9970            (0, 0), // stream_bytes
9971            (0, 0), // total_bytes
9972            4,      // active_streams
9973            false,  // is_last_stream
9974        );
9975
9976        assert_eq!(metrics.bin, 25); // 100 / 4
9977        assert_eq!(metrics.bout, 50); // 200 / 4
9978        assert_eq!(overhead_bin, 75);
9979        assert_eq!(overhead_bout, 150);
9980    }
9981
9982    #[test]
9983    fn test_distribute_overhead_clamps_to_remaining() {
9984        let mut metrics = SessionMetrics::new(None);
9985        let mut overhead_bin = 10;
9986        let mut overhead_bout = 10;
9987
9988        // Stream claims 100% of bytes but overhead is small (last stream)
9989        distribute_overhead(
9990            &mut metrics,
9991            &mut overhead_bin,
9992            &mut overhead_bout,
9993            (1000, 1000), // stream_bytes
9994            (1000, 1000), // total_bytes
9995            1,            // active_streams
9996            true,         // is_last_stream
9997        );
9998
9999        assert_eq!(metrics.bin, 10);
10000        assert_eq!(metrics.bout, 10);
10001        assert_eq!(overhead_bin, 0);
10002        assert_eq!(overhead_bout, 0);
10003    }
10004
10005    #[test]
10006    fn test_distribute_overhead_zero_active_streams() {
10007        let mut metrics = SessionMetrics::new(None);
10008        let mut overhead_bin = 100;
10009        let mut overhead_bout = 100;
10010
10011        // 0 active streams (edge case) — last stream gets all remainder
10012        distribute_overhead(
10013            &mut metrics,
10014            &mut overhead_bin,
10015            &mut overhead_bout,
10016            (0, 0),
10017            (0, 0),
10018            0,
10019            true,
10020        );
10021
10022        assert_eq!(metrics.bin, 100); // last stream gets all remaining
10023        assert_eq!(metrics.bout, 100);
10024        assert_eq!(overhead_bin, 0);
10025        assert_eq!(overhead_bout, 0);
10026    }
10027
10028    #[test]
10029    fn test_distribute_overhead_last_stream_gets_remainder() {
10030        let mut metrics1 = SessionMetrics::new(None);
10031        let mut metrics2 = SessionMetrics::new(None);
10032        let mut overhead_bin = 120;
10033        let mut overhead_bout = 120;
10034
10035        // First stream (not last): gets proportional share
10036        distribute_overhead(
10037            &mut metrics1,
10038            &mut overhead_bin,
10039            &mut overhead_bout,
10040            (100, 100), // stream_bytes
10041            (300, 300), // total_bytes
10042            3,          // active_streams
10043            false,      // is_last_stream
10044        );
10045
10046        let remaining_bin = overhead_bin;
10047        let remaining_bout = overhead_bout;
10048
10049        // Last stream: gets ALL remaining overhead (no rounding loss)
10050        distribute_overhead(
10051            &mut metrics2,
10052            &mut overhead_bin,
10053            &mut overhead_bout,
10054            (100, 100), // stream_bytes
10055            (300, 300), // total_bytes
10056            3,          // active_streams
10057            true,       // is_last_stream
10058        );
10059
10060        assert_eq!(metrics2.bin, remaining_bin);
10061        assert_eq!(metrics2.bout, remaining_bout);
10062        assert_eq!(overhead_bin, 0, "no remainder bytes should be lost");
10063        assert_eq!(overhead_bout, 0, "no remainder bytes should be lost");
10064    }
10065
10066    // ── H2FlowControl (additional edge cases) ─────────────────────────
10067
10068    #[test]
10069    fn test_flow_control_queue_window_update_cap() {
10070        // Verify DEFAULT_MAX_PENDING_WINDOW_UPDATES reflects 1 + 4*MAX_CONCURRENT_STREAMS
10071        assert_eq!(DEFAULT_MAX_PENDING_WINDOW_UPDATES, 1 + 100 * 4);
10072
10073        // Simulate queue reaching capacity
10074        let cap = DEFAULT_MAX_PENDING_WINDOW_UPDATES;
10075        let mut updates: HashMap<u32, u32> = HashMap::new();
10076        for i in 0..cap as u32 {
10077            updates.insert(i, 1000);
10078        }
10079        assert_eq!(updates.len(), cap);
10080
10081        // A new stream ID beyond capacity should be rejected
10082        let next_stream = cap as u32;
10083        let at_cap = updates.len() >= cap;
10084        assert!(at_cap);
10085        assert!(!updates.contains_key(&next_stream));
10086
10087        // Verify custom max_concurrent_streams produces proportional cap
10088        let custom_cap = 1 + 500_usize * 4;
10089        assert_eq!(custom_cap, 2001);
10090    }
10091
10092    #[test]
10093    fn test_h2_connection_config_defaults() {
10094        let config = H2ConnectionConfig::default();
10095        assert_eq!(config.initial_connection_window, ENLARGED_CONNECTION_WINDOW);
10096        assert_eq!(
10097            config.max_concurrent_streams,
10098            DEFAULT_MAX_CONCURRENT_STREAMS
10099        );
10100        assert_eq!(config.stream_shrink_ratio, 2);
10101    }
10102
10103    #[test]
10104    fn test_h2_connection_config_clamp_window_lower_bound() {
10105        // Below minimum: clamped to DEFAULT_INITIAL_WINDOW_SIZE (65535)
10106        let config = H2ConnectionConfig::new(100, 100, 2);
10107        assert_eq!(
10108            config.initial_connection_window,
10109            DEFAULT_INITIAL_WINDOW_SIZE
10110        );
10111    }
10112
10113    #[test]
10114    fn test_h2_connection_config_clamp_window_upper_bound() {
10115        // Above maximum: clamped to FLOW_CONTROL_MAX_WINDOW (2^31-1)
10116        let config = H2ConnectionConfig::new(u32::MAX, 100, 2);
10117        assert_eq!(config.initial_connection_window, FLOW_CONTROL_MAX_WINDOW);
10118    }
10119
10120    #[test]
10121    fn test_h2_connection_config_clamp_window_exact_minimum() {
10122        // Exactly minimum: no clamping, no zero-increment WINDOW_UPDATE risk
10123        let config = H2ConnectionConfig::new(DEFAULT_INITIAL_WINDOW_SIZE, 100, 2);
10124        assert_eq!(
10125            config.initial_connection_window,
10126            DEFAULT_INITIAL_WINDOW_SIZE
10127        );
10128        // Increment to send would be 0 — the code guards this with `if increment > 0`
10129        let increment = config
10130            .initial_connection_window
10131            .saturating_sub(DEFAULT_INITIAL_WINDOW_SIZE);
10132        assert_eq!(increment, 0);
10133    }
10134
10135    #[test]
10136    fn test_h2_connection_config_clamp_shrink_ratio() {
10137        // Below minimum: clamped to 2 (1 would defeat recycling)
10138        let config = H2ConnectionConfig::new(ENLARGED_CONNECTION_WINDOW, 100, 0);
10139        assert_eq!(config.stream_shrink_ratio, 2);
10140        let config = H2ConnectionConfig::new(ENLARGED_CONNECTION_WINDOW, 100, 1);
10141        assert_eq!(config.stream_shrink_ratio, 2);
10142    }
10143
10144    #[test]
10145    fn test_h2_connection_config_clamp_concurrent_streams() {
10146        let config = H2ConnectionConfig::new(ENLARGED_CONNECTION_WINDOW, 0, 2);
10147        assert_eq!(config.max_concurrent_streams, 1);
10148    }
10149
10150    #[test]
10151    fn test_h2_connection_config_from_optional_uses_defaults() {
10152        let config = H2ConnectionConfig::from_optional(None, None, None);
10153        let defaults = H2ConnectionConfig::default();
10154        assert_eq!(config, defaults);
10155    }
10156
10157    #[test]
10158    fn test_h2_connection_config_from_optional_overrides() {
10159        let config = H2ConnectionConfig::from_optional(Some(2_000_000), Some(500), Some(4));
10160        assert_eq!(config.initial_connection_window, 2_000_000);
10161        assert_eq!(config.max_concurrent_streams, 500);
10162        assert_eq!(config.stream_shrink_ratio, 4);
10163    }
10164
10165    // The old settings-change-negative-window test was removed here, not
10166    // moved: its docstring premise was wrong. `update_initial_window_size`
10167    // (below) only ever adjusts *per-stream* windows on a
10168    // SETTINGS_INITIAL_WINDOW_SIZE change, never the connection-level
10169    // `flow_control.window` that test constructed and mutated directly — so
10170    // it never exercised a real code path. `h2_flow_control.rs`'s test
10171    // `consume_send_window_saturates_at_i32_min_without_wrapping` covers a
10172    // genuinely different, previously-untested boundary of the same
10173    // `consume_send_window` API (the `saturating_sub` floor at `i32::MIN`);
10174    // it does not restore the deleted test's coverage, because the deleted
10175    // test covered nothing real.
10176
10177    #[test]
10178    fn test_flow_control_coalesce_saturates_at_max_increment() {
10179        let max_increment = i32::MAX as u32;
10180        let mut updates: HashMap<u32, u32> = HashMap::new();
10181
10182        // Insert at max and try to coalesce more
10183        updates.insert(1, max_increment);
10184        if let Some(existing) = updates.get_mut(&1) {
10185            *existing = existing.saturating_add(1000).min(max_increment);
10186        }
10187        assert_eq!(*updates.get(&1).unwrap(), max_increment);
10188    }
10189
10190    // ── H2FloodConfig (additional) ───────────────────────────────────
10191    //
10192    // Moved to `h2_flood_detector.rs`'s `#[cfg(test)] mod tests`
10193    // (`test_flood_config_default_matches_constants`, `test_flood_config_equality`).
10194
10195    // ── distribute_overhead (additional edge cases) ───────────────────
10196
10197    #[test]
10198    fn test_distribute_overhead_asymmetric_in_out() {
10199        let mut metrics = SessionMetrics::new(None);
10200        let mut overhead_bin = 1000;
10201        let mut overhead_bout = 1000;
10202
10203        // Stream transferred 100% inbound, 0% outbound (not last stream)
10204        distribute_overhead(
10205            &mut metrics,
10206            &mut overhead_bin,
10207            &mut overhead_bout,
10208            (500, 0),   // stream_bytes
10209            (500, 100), // total_bytes
10210            2,          // active_streams
10211            false,      // is_last_stream
10212        );
10213
10214        assert_eq!(metrics.bin, 1000); // 100% of inbound overhead
10215        assert_eq!(metrics.bout, 0); // 0% of outbound overhead
10216        assert_eq!(overhead_bin, 0);
10217        assert_eq!(overhead_bout, 1000);
10218    }
10219
10220    #[test]
10221    fn test_distribute_overhead_many_streams_accumulate() {
10222        let mut metrics = SessionMetrics::new(None);
10223        let mut overhead_bin = 120;
10224        let mut overhead_bout = 120;
10225
10226        // Three equal streams, each calling distribute_overhead.
10227        // With is_last_stream on the third call, the last stream gets all
10228        // remaining overhead, so no rounding loss occurs.
10229        //   call 1: 120 * 100/300 = 40 -> remaining 80
10230        //   call 2:  80 * 100/300 = 26 -> remaining 54
10231        //   call 3: last stream gets all remaining = 54
10232        // Total distributed: 40 + 26 + 54 = 120 (no loss)
10233        for i in 0..3 {
10234            distribute_overhead(
10235                &mut metrics,
10236                &mut overhead_bin,
10237                &mut overhead_bout,
10238                (100, 100), // stream_bytes
10239                (300, 300), // total_bytes
10240                3,          // active_streams
10241                i == 2,     // is_last_stream on final call
10242            );
10243        }
10244
10245        assert_eq!(metrics.bin, 120);
10246        assert_eq!(metrics.bout, 120);
10247        // No rounding residual — last stream absorbed the remainder
10248        assert_eq!(overhead_bin, 0);
10249        assert_eq!(overhead_bout, 0);
10250    }
10251
10252    // ── Hex chunk formatting ────────────────────────────────────────────
10253
10254    /// Verify that the Vec<u8> + write!() hex formatting used in
10255    /// handle_data_frame produces output identical to format!("{:x}").
10256    #[test]
10257    fn test_hex_chunk_length_formatting() {
10258        use std::io::Write as _;
10259
10260        let cases: &[(usize, &[u8])] = &[
10261            (1, b"1"),
10262            (15, b"f"),
10263            (16, b"10"),
10264            (255, b"ff"),
10265            (256, b"100"),
10266            (4096, b"1000"),
10267            (65535, b"ffff"),
10268            (65536, b"10000"),
10269        ];
10270
10271        for &(payload_len, expected) in cases {
10272            let mut buf = Vec::with_capacity(16);
10273            let _ = write!(buf, "{payload_len:x}");
10274            assert_eq!(
10275                buf, expected,
10276                "hex formatting mismatch for payload_len={payload_len}"
10277            );
10278        }
10279
10280        // usize::MAX tested separately to avoid temporary lifetime issue
10281        let max_expected = format!("{:x}", usize::MAX);
10282        let mut buf = Vec::with_capacity(16);
10283        let _ = write!(buf, "{:x}", usize::MAX);
10284        assert_eq!(buf, max_expected.as_bytes());
10285    }
10286
10287    // ── Stream-ID allocation / exhaustion ──────────────────────────────────
10288
10289    /// A fresh client connection starts with `last_stream_id == 0`. The first
10290    /// call MUST issue stream `1` (odd, RFC 9113 §5.1.1) and advance the
10291    /// watermark to `2`.
10292    #[test]
10293    fn test_next_stream_id_client_first_allocation() {
10294        let (issued, next) = next_stream_id(0, true).expect("fresh client must allocate");
10295        assert_eq!(issued, 1);
10296        assert_eq!(next, 2);
10297    }
10298
10299    /// Client allocation yields strictly increasing odd identifiers
10300    /// (1, 3, 5, ...) as required by RFC 9113 §5.1.1.
10301    #[test]
10302    fn test_next_stream_id_client_sequence_is_odd_and_monotonic() {
10303        let mut last = 0u32;
10304        let mut issued_ids = Vec::with_capacity(8);
10305        for _ in 0..8 {
10306            let (id, next) = next_stream_id(last, true).expect("unexhausted");
10307            assert_eq!(id & 1, 1, "client stream ids must be odd (RFC 9113 §5.1.1)");
10308            assert!(issued_ids.last().is_none_or(|prev: &u32| id > *prev));
10309            issued_ids.push(id);
10310            last = next;
10311        }
10312        assert_eq!(issued_ids, vec![1, 3, 5, 7, 9, 11, 13, 15]);
10313    }
10314
10315    /// Server-side allocation yields even identifiers. The helper
10316    /// convention is `watermark - 2` for server, `watermark - 1` for client,
10317    /// so both sides share the same monotonically-increasing even watermark.
10318    /// Sōzu never server-pushes, but the helper must be symmetric so push
10319    /// could be enabled without a regression.
10320    #[test]
10321    fn test_next_stream_id_server_is_even() {
10322        // `last = 2` means the most recent allocation advanced the watermark
10323        // to 2; server then issues `2 - 2 = 0`. This is an artefact of the
10324        // shared watermark and only matters in tests — server never uses it.
10325        let (issued, next) = next_stream_id(2, false).expect("server allocation");
10326        assert_eq!(issued & 1, 0, "server stream ids must be even");
10327        assert_eq!(next, 4);
10328        assert_eq!(issued, 2);
10329
10330        let (issued, next) = next_stream_id(next, false).expect("second slot");
10331        assert_eq!(issued, 4);
10332        assert_eq!(issued & 1, 0);
10333        assert_eq!(next, 6);
10334    }
10335
10336    /// The last client-issuable odd stream ID is `STREAM_ID_MAX = 0x7FFF_FFFF`.
10337    /// To issue it the watermark must advance to `STREAM_ID_MAX + 1 = 2³¹`;
10338    /// the caller therefore supplies `last = STREAM_ID_MAX - 1 = 0x7FFF_FFFE`.
10339    /// That call MUST succeed and return the max ID; the post-call watermark
10340    /// sits at `2³¹`, which is the sentinel that makes the next call fail.
10341    #[test]
10342    fn test_next_stream_id_client_final_slot_allocates() {
10343        let last = STREAM_ID_MAX - 1;
10344        let (issued, next) = next_stream_id(last, true).expect("final slot still allocates");
10345        assert_eq!(issued, STREAM_ID_MAX);
10346        assert_eq!(next, STREAM_ID_MAX + 1);
10347        // And the very next call MUST refuse rather than wrap.
10348        assert!(next_stream_id(next, true).is_none());
10349    }
10350
10351    /// Exhaustion case: once the client has issued stream ID `STREAM_ID_MAX`,
10352    /// the watermark sits at `STREAM_ID_MAX + 1`. The next request MUST return
10353    /// `None` — without this guard the helper would issue `STREAM_ID_MAX + 2`
10354    /// (wrapped down to an even id), which would (a) use the reserved
10355    /// high bit and (b) violate the odd-parity invariant for client streams.
10356    #[test]
10357    fn test_next_stream_id_client_exhausted_returns_none() {
10358        let last = STREAM_ID_MAX + 1;
10359        assert!(next_stream_id(last, true).is_none());
10360    }
10361
10362    /// Exhaustion via `checked_add` saturation: defence in depth in case a
10363    /// caller jumps `last_stream_id` close to `u32::MAX`. The helper must
10364    /// not panic nor overflow — it must return `None`.
10365    #[test]
10366    fn test_next_stream_id_saturates_near_u32_max() {
10367        assert!(next_stream_id(u32::MAX, true).is_none());
10368        assert!(next_stream_id(u32::MAX - 1, true).is_none());
10369    }
10370
10371    /// Server-side exhaustion: same guard, even-parity identifier space.
10372    #[test]
10373    fn test_next_stream_id_server_exhausted_returns_none() {
10374        let last = STREAM_ID_MAX + 1;
10375        assert!(next_stream_id(last, false).is_none());
10376    }
10377
10378    /// Regression guard: the helper must never issue a stream ID that
10379    /// exceeds `STREAM_ID_MAX` for either side, no matter where the
10380    /// watermark sits. This walks every value in a neighbourhood of the
10381    /// boundary to rule out off-by-one errors.
10382    #[test]
10383    fn test_next_stream_id_never_exceeds_stream_id_max() {
10384        for last in (STREAM_ID_MAX - 4)..=(STREAM_ID_MAX + 4) {
10385            for is_client in [true, false] {
10386                if let Some((issued, next)) = next_stream_id(last, is_client) {
10387                    assert!(
10388                        issued <= STREAM_ID_MAX,
10389                        "issued id {issued} exceeds STREAM_ID_MAX (last={last}, is_client={is_client})"
10390                    );
10391                    // `next` is the post-allocation watermark and may sit at
10392                    // STREAM_ID_MAX + 1 — the very next call must then return None.
10393                    if next > STREAM_ID_MAX {
10394                        assert!(
10395                            next_stream_id(next, is_client).is_none(),
10396                            "second call after final slot must report exhaustion"
10397                        );
10398                    }
10399                }
10400            }
10401        }
10402    }
10403
10404    /// The helper's `is_client` flag must cleanly split the ID space so that
10405    /// a client and a server peered on the same connection cannot collide.
10406    /// Given the same `last_stream_id`, the two parities must differ by 1.
10407    #[test]
10408    fn test_next_stream_id_client_server_parities_disjoint() {
10409        for last in [0u32, 2, 4, 10, 100, 1_000_000, STREAM_ID_MAX - 3] {
10410            let (client_id, _) = next_stream_id(last, true).unwrap();
10411            let (server_id, _) = next_stream_id(last, false).unwrap();
10412            assert_eq!(client_id & 1, 1);
10413            assert_eq!(server_id & 1, 0);
10414            assert_eq!(client_id.abs_diff(server_id), 1);
10415        }
10416    }
10417
10418    // ── LIFECYCLE §9 invariant 16: any_stream_id_matches ─────────────────
10419    //
10420    // Covers the iteration dispatch used by `any_stream_has_pending_back`.
10421    // Testing the probe directly against a synthetic closure keeps the
10422    // tests independent of the full `Stream` fixture (which requires a
10423    // `Pool` and a fully-built `HttpContext`).
10424
10425    #[test]
10426    fn test_any_stream_id_matches_empty_map_is_false() {
10427        let streams: BTreeMap<StreamId, GlobalStreamId> = BTreeMap::new();
10428        assert!(!any_stream_id_matches(&streams, |_| true));
10429    }
10430
10431    #[test]
10432    fn test_any_stream_id_matches_all_probe_false_is_false() {
10433        let mut streams: BTreeMap<StreamId, GlobalStreamId> = BTreeMap::new();
10434        streams.insert(1, 0);
10435        streams.insert(3, 1);
10436        streams.insert(5, 2);
10437        assert!(!any_stream_id_matches(&streams, |_| false));
10438    }
10439
10440    #[test]
10441    fn test_any_stream_id_matches_any_probe_true_is_true() {
10442        let mut streams: BTreeMap<StreamId, GlobalStreamId> = BTreeMap::new();
10443        streams.insert(1, 0);
10444        streams.insert(3, 1);
10445        streams.insert(5, 2);
10446        // Probe is true only for GlobalStreamId == 1 (i.e. StreamId 3).
10447        assert!(any_stream_id_matches(&streams, |gid| gid == 1));
10448    }
10449
10450    #[test]
10451    fn test_any_stream_id_matches_single_entry() {
10452        let mut streams: BTreeMap<StreamId, GlobalStreamId> = BTreeMap::new();
10453        streams.insert(42, 7);
10454        assert!(any_stream_id_matches(&streams, |gid| gid == 7));
10455        assert!(!any_stream_id_matches(&streams, |gid| gid == 8));
10456    }
10457
10458    #[test]
10459    fn test_any_stream_id_matches_short_circuits() {
10460        let mut streams: BTreeMap<StreamId, GlobalStreamId> = BTreeMap::new();
10461        streams.insert(1, 0);
10462        streams.insert(3, 1);
10463        streams.insert(5, 2);
10464        streams.insert(7, 3);
10465        let mut calls = 0usize;
10466        let result = any_stream_id_matches(&streams, |_| {
10467            calls += 1;
10468            true
10469        });
10470        assert!(result);
10471        // `Iterator::any` short-circuits on the first `true` — so the probe
10472        // must fire at most once in this construction.
10473        assert_eq!(calls, 1);
10474    }
10475
10476    // ── cumulative-stall budget decision (fc_stall_budget_decision) ──
10477
10478    #[test]
10479    fn test_fc_stall_budget_open_window_always_clears() {
10480        // A genuinely open send window is a real un-stall, regardless of prior
10481        // accumulated progress or this pass's drain.
10482        assert_eq!(
10483            fc_stall_budget_decision(false, 0, None),
10484            FcStallAction::Clear
10485        );
10486        assert_eq!(
10487            fc_stall_budget_decision(false, 1, Some(5)),
10488            FcStallAction::Clear
10489        );
10490        assert_eq!(
10491            fc_stall_budget_decision(false, i32::MAX, Some(FC_STALL_CLEAR_FLOOR)),
10492            FcStallAction::Clear
10493        );
10494    }
10495
10496    #[test]
10497    fn test_fc_stall_budget_blocked_arms_and_accumulates() {
10498        // First blocked pass arms with this pass's drain.
10499        assert_eq!(
10500            fc_stall_budget_decision(true, 1, None),
10501            FcStallAction::Arm { progress: 1 }
10502        );
10503        // A blocked pass with no drain keeps the accumulator unchanged, so the
10504        // deadline keeps aging (a window-0 stall makes consumed == 0).
10505        assert_eq!(
10506            fc_stall_budget_decision(true, 0, Some(42)),
10507            FcStallAction::Arm { progress: 42 }
10508        );
10509        // Negative `consumed` is clamped to 0 (defensive; converter.window only
10510        // shrinks, so consumed is >= 0 in practice).
10511        assert_eq!(
10512            fc_stall_budget_decision(true, -10, Some(7)),
10513            FcStallAction::Arm { progress: 7 }
10514        );
10515    }
10516
10517    #[test]
10518    fn test_fc_stall_budget_floor_clears() {
10519        // Reaching the floor in a single pass (a full DATA frame of real
10520        // delivery) clears the deadline.
10521        assert_eq!(
10522            fc_stall_budget_decision(true, FC_STALL_CLEAR_FLOOR as i32, None),
10523            FcStallAction::Clear
10524        );
10525        // Exactly one byte below the floor still arms.
10526        assert_eq!(
10527            fc_stall_budget_decision(true, (FC_STALL_CLEAR_FLOOR - 1) as i32, None),
10528            FcStallAction::Arm {
10529                progress: FC_STALL_CLEAR_FLOOR - 1
10530            }
10531        );
10532        // Prior progress plus this pass crossing the floor clears.
10533        assert_eq!(
10534            fc_stall_budget_decision(true, 1, Some(FC_STALL_CLEAR_FLOOR - 1)),
10535            FcStallAction::Clear
10536        );
10537    }
10538
10539    #[test]
10540    fn test_fc_stall_budget_wu_drip_ages_until_floor() {
10541        // The WINDOW_UPDATE(+1) closure: a 1-byte-per-pass drip must keep the
10542        // deadline armed (aging) for the whole run up to the floor and only
10543        // clear on the pass that reaches it — so a drip granting < floor bytes
10544        // per idle period is reaped, never kept alive. This is the unit-level
10545        // proof that the budget closes the WINDOW_UPDATE-drip vector.
10546        let mut progress: Option<usize> = None;
10547        for pass in 1..FC_STALL_CLEAR_FLOOR {
10548            match fc_stall_budget_decision(true, 1, progress) {
10549                FcStallAction::Arm { progress: p } => {
10550                    assert_eq!(p, pass, "drip accumulator off at pass {pass}");
10551                    progress = Some(p);
10552                }
10553                FcStallAction::Clear => panic!("drip cleared the deadline early at pass {pass}"),
10554            }
10555        }
10556        // The pass that reaches the floor finally clears.
10557        assert_eq!(
10558            fc_stall_budget_decision(true, 1, progress),
10559            FcStallAction::Clear
10560        );
10561    }
10562
10563    // ── flow-control-stall reaper union (h2_stream_table::collect_timed_out) ──
10564    //
10565    // The two-guard union (bidirectional-silence + flow-control-stall),
10566    // its dedup, its rst_sent/untracked filtering, and its determinism
10567    // (ascending stream_id order — the regression test for this
10568    // extraction's fix) are now unit-tested directly against
10569    // `H2StreamTable::collect_timed_out` in `h2_stream_table.rs`'s own test
10570    // module, without needing a full `ConnectionH2` fixture.
10571
10572    // ── LIFECYCLE §9 invariant 16: any_stream_has_pending_back ───────────
10573
10574    /// Build a minimal `Stream` for invariant-16 probing. Uses the pool
10575    /// plumbing so `back.blocks` / `back.out` exist; every other field is
10576    /// default-valued because the predicate only reads the back buffer.
10577    fn make_stream_for_invariant_16(pool: &Rc<RefCell<Pool>>, session_ulid: Ulid) -> Stream {
10578        let http_ctx = HttpContext {
10579            keep_alive_backend: true,
10580            keep_alive_frontend: true,
10581            sticky_session_found: None,
10582            method: None,
10583            authority: None,
10584            path: None,
10585            status: None,
10586            reason: None,
10587            user_agent: None,
10588            x_request_id: None,
10589            xff_chain: None,
10590            #[cfg(feature = "opentelemetry")]
10591            otel: None,
10592            closing: false,
10593            session_id: session_ulid,
10594            id: Ulid::generate(),
10595            backend_id: None,
10596            cluster_id: None,
10597            cluster_metrics_incarnation: Default::default(),
10598            affinity_key: None,
10599            protocol: Protocol::HTTPS,
10600            public_address: "127.0.0.1:0".parse().unwrap(),
10601            session_address: None,
10602            sticky_name: String::new(),
10603            sticky_session: None,
10604            backend_address: None,
10605            tls_server_name: None,
10606            tls_cert_names: None,
10607            strict_sni_binding: false,
10608            elide_x_real_ip: false,
10609            send_x_real_ip: false,
10610            forwarded_headers: sozu_command_lib::proto::command::ForwardedHeaders::Both,
10611            max_trailer_fields: 128,
10612            trailer_fields: 0,
10613            tls_version: None,
10614            tls_cipher: None,
10615            tls_alpn: None,
10616            sozu_id_header: String::from("Sozu-Id"),
10617            redirect_location: None,
10618            www_authenticate: None,
10619            original_authority: None,
10620            headers_response: Vec::new(),
10621            retry_after_seconds: None,
10622            frontend_redirect_template: None,
10623            redirect_status: None,
10624            tags: None,
10625            forwarding_hop: None,
10626            access_log_message: None,
10627            backends_unavailable: false,
10628        };
10629        Stream::new(
10630            &mut PoolBufferSource::new(Rc::downgrade(pool)),
10631            http_ctx,
10632            crate::protocol::mux::test_support::test_answers(),
10633            65_535,
10634        )
10635        .expect("pool should have capacity for two buffers")
10636    }
10637
10638    fn make_pool_for_invariant_16() -> Rc<RefCell<Pool>> {
10639        // Two buffer slots per stream (front + back), ten stream slots is
10640        // plenty for the tests below.
10641        Rc::new(RefCell::new(Pool::with_capacity(4, 20, 16_384)))
10642    }
10643
10644    /// What `handle_data_frame` does with the DATA payload of a backend
10645    /// response: a 204 or a 304 cannot carry content and is reset, even to
10646    /// HEAD; a response to HEAD only SHOULD NOT and its payload is
10647    /// discarded (RFC 9110 §6.4.1, §9.3.2, §15.3.5, §15.4.5).
10648    #[test]
10649    fn a_backend_response_content_is_forbidden_for_204_and_304_and_discarded_for_head() {
10650        use crate::protocol::kawa_h1::parser::Method;
10651        let pool = make_pool_for_invariant_16();
10652        let mut stream = make_stream_for_invariant_16(&pool, Ulid::generate());
10653        let cases = [
10654            (
10655                Some(Method::Head),
10656                Some(200),
10657                BackendResponseContent::Discarded,
10658            ),
10659            (
10660                Some(Method::Head),
10661                Some(204),
10662                BackendResponseContent::Forbidden,
10663            ),
10664            (
10665                Some(Method::Get),
10666                Some(204),
10667                BackendResponseContent::Forbidden,
10668            ),
10669            (
10670                Some(Method::Get),
10671                Some(304),
10672                BackendResponseContent::Forbidden,
10673            ),
10674            (
10675                Some(Method::Get),
10676                Some(200),
10677                BackendResponseContent::Forwarded,
10678            ),
10679            (
10680                Some(Method::Post),
10681                Some(205),
10682                BackendResponseContent::Forwarded,
10683            ),
10684            (None, None, BackendResponseContent::Forwarded),
10685        ];
10686        for (method, status, expected) in cases {
10687            stream.context.method = method.clone();
10688            stream.context.status = status;
10689            assert_eq!(
10690                ConnectionH2::backend_response_content(&stream.context),
10691                expected,
10692                "{method:?} {status:?}"
10693            );
10694        }
10695    }
10696
10697    #[test]
10698    fn test_any_stream_has_pending_back_empty_map_is_false() {
10699        let pool = make_pool_for_invariant_16();
10700        let ulid = Ulid::generate();
10701        let streams_map: BTreeMap<StreamId, GlobalStreamId> = BTreeMap::new();
10702        let context_streams = vec![make_stream_for_invariant_16(&pool, ulid)];
10703        assert!(!any_stream_has_pending_back(&streams_map, &context_streams));
10704    }
10705
10706    #[test]
10707    fn test_any_stream_has_pending_back_all_drained_is_false() {
10708        let pool = make_pool_for_invariant_16();
10709        let ulid = Ulid::generate();
10710        let context_streams = vec![
10711            make_stream_for_invariant_16(&pool, ulid),
10712            make_stream_for_invariant_16(&pool, ulid),
10713        ];
10714        let mut streams_map: BTreeMap<StreamId, GlobalStreamId> = BTreeMap::new();
10715        streams_map.insert(1, 0);
10716        streams_map.insert(3, 1);
10717        // Both freshly-built streams have empty back.out and back.blocks
10718        // (Kawa::new starts with empty deques).
10719        assert!(!any_stream_has_pending_back(&streams_map, &context_streams));
10720    }
10721
10722    #[test]
10723    fn test_any_stream_has_pending_back_unknown_gid_is_false() {
10724        // LIFECYCLE invariant 16 defence-in-depth: an unknown
10725        // `GlobalStreamId` during a stream-removal race must not panic;
10726        // `.get()` must short-circuit to `false`.
10727        let pool = make_pool_for_invariant_16();
10728        let ulid = Ulid::generate();
10729        let context_streams = vec![make_stream_for_invariant_16(&pool, ulid)];
10730        let mut streams_map: BTreeMap<StreamId, GlobalStreamId> = BTreeMap::new();
10731        // GlobalStreamId 42 is out of range for the 1-element slice above.
10732        streams_map.insert(7, 42);
10733        assert!(!any_stream_has_pending_back(&streams_map, &context_streams));
10734    }
10735
10736    #[test]
10737    fn test_any_stream_has_pending_back_with_pending_blocks_is_true() {
10738        let pool = make_pool_for_invariant_16();
10739        let ulid = Ulid::generate();
10740        let mut stream = make_stream_for_invariant_16(&pool, ulid);
10741        // Push one dummy block — any Block variant is fine; the predicate
10742        // only checks `blocks.is_empty()`.
10743        stream.back.blocks.push_back(kawa::Block::StatusLine);
10744        let mut streams_map: BTreeMap<StreamId, GlobalStreamId> = BTreeMap::new();
10745        streams_map.insert(1, 0);
10746        assert!(any_stream_has_pending_back(&streams_map, &[stream]));
10747    }
10748
10749    #[test]
10750    fn test_any_stream_has_pending_back_with_pending_out_is_true() {
10751        let pool = make_pool_for_invariant_16();
10752        let ulid = Ulid::generate();
10753        let mut stream = make_stream_for_invariant_16(&pool, ulid);
10754        // Non-empty out buffer with no blocks.
10755        stream
10756            .back
10757            .out
10758            .push_back(kawa::OutBlock::Store(kawa::Store::Static(b"partial frame")));
10759        let mut streams_map: BTreeMap<StreamId, GlobalStreamId> = BTreeMap::new();
10760        streams_map.insert(1, 0);
10761        assert!(any_stream_has_pending_back(&streams_map, &[stream]));
10762    }
10763
10764    // ── Mid-pass ready-incremental census consistency ───────────────────
10765    //
10766    // The three scalar bucket-decrement tests moved to `h2_scheduler.rs`.
10767    // They were named after `ready_incremental_by_urgency`, the local
10768    // `HashMap` this step replaced with `ReadyIncrementalCensus`; no symbol
10769    // of that name exists anywhere in the tree any more.
10770    // They used to re-implement the `saturating_sub` + bucket-scoped
10771    // decrement inline in the test body — a copy of the scheduler's
10772    // arithmetic, which could not catch the scheduler getting it wrong.
10773    // They now call `ReadyIncrementalCensus::note_ineligible` and
10774    // `::incremental_peer_count`, which is the code `write_streams` runs.
10775
10776    // ── h2.streams.ready_incremental.by_urgency aggregate ────────────────
10777
10778    /// Read the process-local `h2.streams.ready_incremental.by_urgency` gauge,
10779    /// treating an absent key as 0. `dump_local_proxy_metrics` is a
10780    /// non-draining filter over the proxy `MetricsMap`, so repeated reads are
10781    /// side-effect free and the key is the raw metric name.
10782    fn ready_incremental_gauge() -> i64 {
10783        use sozu_command::proto::command::filtered_metrics::Inner;
10784        crate::metrics::METRICS.with(|metrics| {
10785            metrics
10786                .borrow_mut()
10787                .dump_local_proxy_metrics()
10788                .get(names::h2::STREAMS_READY_INCREMENTAL_BY_URGENCY)
10789                .and_then(|fm| fm.inner.as_ref())
10790                .and_then(|inner| match inner {
10791                    Inner::Gauge(v) => Some(*v as i64),
10792                    _ => None,
10793                })
10794                .unwrap_or(0)
10795        })
10796    }
10797
10798    /// A [`BufferSource`] that hands over pooled wire buffers exactly as the
10799    /// real one does but refuses every scratch request.
10800    ///
10801    /// It exists to separate the two halves of the contract. Refusing
10802    /// `checkout` is already reachable by sizing a pool, and the H2
10803    /// simulator's exhaustion scenario does exactly that. Refusing `scratch`
10804    /// is reachable ONLY through a caller-supplied source: the proxy's own
10805    /// [`PoolBufferSource`] hands out a fresh `Vec` and never says no. That
10806    /// asymmetry is the point of the trait being caller-implemented rather
10807    /// than a method on `Pool`, so it needs a test that could not be written
10808    /// without it.
10809    struct ScratchStarvedSource(PoolBufferSource);
10810
10811    impl super::super::buffer_source::BufferSource for ScratchStarvedSource {
10812        fn checkout(&mut self) -> Option<crate::pool::Checkout> {
10813            self.0.checkout()
10814        }
10815
10816        fn scratch(&mut self) -> Option<Vec<u8>> {
10817            None
10818        }
10819    }
10820
10821    fn loopback_socket_handle() -> mio::net::TcpStream {
10822        mio::net::TcpStream::connect(
10823            format!("127.0.0.1:{}", crate::testing::provide_port())
10824                .parse()
10825                .expect("loopback address must parse"),
10826        )
10827        .expect("mio connect must return a socket handle")
10828    }
10829
10830    fn connection_from_source(
10831        buffers: &mut dyn super::super::buffer_source::BufferSource,
10832    ) -> Option<H2Shell<mio::net::TcpStream>> {
10833        H2Shell::new(
10834            Ulid::generate(),
10835            loopback_socket_handle(),
10836            Position::Server,
10837            buffers,
10838            H2FloodConfig::default(),
10839            H2ConnectionConfig::default(),
10840            Duration::from_secs(30),
10841            None,
10842            Duration::from_secs(30),
10843            Some((H2StreamId::Zero, CLIENT_PREFACE_SIZE)),
10844            Ready::READABLE | Ready::HUP | Ready::ERROR,
10845        )
10846    }
10847
10848    /// The HPACK half of the ownership contract is real, not decorative: the
10849    /// codec pair and its scratch buffers are acquired from the connection's
10850    /// buffer source, and a source that refuses one refuses the connection.
10851    ///
10852    /// Both arms run against the SAME pool, with enough buffers for the
10853    /// connection's stream-0 checkout. So the only difference between the two
10854    /// outcomes is the source's answer to `scratch`, which is what stops this
10855    /// from passing for the unrelated reason that the pool ran dry.
10856    #[test]
10857    fn a_source_that_refuses_scratch_refuses_the_connection() {
10858        let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
10859
10860        assert!(
10861            connection_from_source(&mut PoolBufferSource::new(Rc::downgrade(&pool))).is_some(),
10862            "the pool has free buffers, so a source that grants scratch must build a connection",
10863        );
10864        assert!(
10865            connection_from_source(&mut ScratchStarvedSource(PoolBufferSource::new(
10866                Rc::downgrade(&pool)
10867            )))
10868            .is_none(),
10869            "a source that refuses the HPACK scratch must refuse the connection: the codec pair \
10870             is under the same ownership contract as the wire buffers, not allocated behind it",
10871        );
10872    }
10873
10874    /// `h2.streams.ready_incremental.by_urgency` is an aggregate across every
10875    /// live H2 connection, so a connection must hand back exactly what it
10876    /// contributed, on every close path. `H2Shell`'s [`Drop`] is the single
10877    /// decrement site; this pins the round trip.
10878    ///
10879    /// Before this was folded into `last_gauge_snapshot`, the metric was an
10880    /// absolute `gauge!` set from inside `write_streams`: connection B
10881    /// overwrote connection A's value, nothing was ever released, and the
10882    /// dashboard read "whatever the last writer wrote" instead of a sum.
10883    ///
10884    /// `METRICS` is a thread-local, so this asserts the DELTA around one
10885    /// connection's lifetime — robust to any starting value.
10886    ///
10887    /// To SEE THIS RED: delete `self.core.release_connection_gauges();`
10888    /// from `impl Drop for H2Shell`, leaving the
10889    /// `self.record_pending_metrics();` below it. The live-delta assertion
10890    /// still passes — the shell still records what the core queued — and the
10891    /// post-drop one fails with `left: 3, right: 0`: the connection's
10892    /// contribution outliving the connection, which is the
10893    /// aggregate drifting upward by 3 for the rest of the worker's life.
10894    #[test]
10895    fn dropping_a_connection_releases_its_ready_incremental_contribution() {
10896        let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
10897
10898        // A non-blocking connect to a (likely unused) loopback port returns a
10899        // real socket handle immediately, regardless of whether the connection
10900        // completes; nothing below reads or writes it.
10901        let socket = mio::net::TcpStream::connect(
10902            format!("127.0.0.1:{}", crate::testing::provide_port())
10903                .parse()
10904                .expect("loopback address must parse"),
10905        )
10906        .expect("mio connect must return a socket handle");
10907
10908        let before = ready_incremental_gauge();
10909
10910        let mut connection = H2Shell::new(
10911            Ulid::generate(),
10912            socket,
10913            Position::Server,
10914            &mut PoolBufferSource::new(Rc::downgrade(&pool)),
10915            H2FloodConfig::default(),
10916            H2ConnectionConfig::default(),
10917            Duration::from_secs(30),
10918            None,
10919            Duration::from_secs(30),
10920            Some((H2StreamId::Zero, CLIENT_PREFACE_SIZE)),
10921            Ready::READABLE | Ready::HUP | Ready::ERROR,
10922        )
10923        .expect("a pool with free buffers must yield an H2 connection");
10924
10925        // What the tail of `write_streams` writes once the pass's bucket map
10926        // is final, published by the `gauge_connection_state` call that
10927        // follows it — then handed to the aggregator by the shell, which is
10928        // what `H2Shell::settled` does at the end of every real pass.
10929        connection.core.ready_incremental_streams = 3;
10930        connection.core.gauge_connection_state();
10931        connection.record_pending_metrics();
10932
10933        assert_eq!(
10934            ready_incremental_gauge() - before,
10935            3,
10936            "gauge_connection_state must add this connection's ready-incremental \
10937             count to the aggregate as a signed delta"
10938        );
10939
10940        drop(connection);
10941
10942        assert_eq!(
10943            ready_incremental_gauge(),
10944            before,
10945            "Drop must subtract exactly the contribution the connection made, \
10946             returning the aggregate to its prior value"
10947        );
10948    }
10949
10950    /// The core computes the gauge arithmetic and stops there: writing the
10951    /// worker-local `METRICS` aggregator is the shell's step, through
10952    /// [`H2Shell::record_pending_metrics`].
10953    ///
10954    /// This is the property that makes the H2 core drivable by a deterministic
10955    /// simulator, which is the whole point of returning the metric rather than
10956    /// emitting it: a driver with no aggregator sees the same
10957    /// [`MetricEvent`]s that production folds into `METRICS`, and can fold
10958    /// them into a shadow model instead. `protocol/udp/`'s core already works
10959    /// this way and `sim/tests/udp_simulation.rs` already reads it that way.
10960    ///
10961    /// The sibling test above pins the arithmetic; this one pins *who writes*,
10962    /// which no assertion on the aggregate's value can distinguish — a core
10963    /// that emitted directly and a shell that records what the core queued
10964    /// leave the aggregate identical.
10965    ///
10966    /// To SEE THIS RED: in [`ConnectionH2::gauge_connection_state`], put
10967    /// `gauge_add!(names::h2::STREAMS_READY_INCREMENTAL_BY_URGENCY, dr);`
10968    /// back in place of the
10969    /// `self.metric_events.push(MetricEvent::StreamsReadyIncrementalByUrgency(dr));`
10970    /// push. The first assertion then fails with `left: <before + 5>, right:
10971    /// <before>` — the core reaching the aggregator behind the shell's back.
10972    #[test]
10973    fn the_core_queues_gauge_deltas_instead_of_writing_the_metrics_registry() {
10974        let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
10975        let before = ready_incremental_gauge();
10976
10977        let mut connection =
10978            connection_from_source(&mut PoolBufferSource::new(Rc::downgrade(&pool)))
10979                .expect("a pool with free buffers must yield an H2 connection");
10980
10981        connection.core.ready_incremental_streams = 5;
10982        connection.core.gauge_connection_state();
10983
10984        assert_eq!(
10985            ready_incremental_gauge(),
10986            before,
10987            "the core must not write METRICS: `gauge_connection_state` queues \
10988             a MetricEvent and the shell decides when it is recorded"
10989        );
10990
10991        let queued: Vec<MetricEvent> = connection.core.drain_metric_events().collect();
10992        assert!(
10993            queued.contains(&MetricEvent::StreamsReadyIncrementalByUrgency(5)),
10994            "the queued events must carry the signed delta the core computed, \
10995             got {queued:?}"
10996        );
10997        assert_eq!(
10998            ready_incremental_gauge(),
10999            before,
11000            "draining is not recording: taking the events out of the core must \
11001             not move the aggregate either"
11002        );
11003
11004        for event in queued {
11005            record_metric(event);
11006        }
11007
11008        assert_eq!(
11009            ready_incremental_gauge() - before,
11010            5,
11011            "recording the drained events must produce exactly the aggregate \
11012             the old in-core `gauge_add!` produced"
11013        );
11014
11015        drop(connection);
11016
11017        assert_eq!(
11018            ready_incremental_gauge(),
11019            before,
11020            "the released snapshot must travel the same queue and rebalance to \
11021             the prior value"
11022        );
11023    }
11024
11025    /// Read a process-local counter by its raw metric key, treating an absent
11026    /// key as 0. Counter sibling of [`ready_incremental_gauge`], and
11027    /// `dump_local_proxy_metrics` is non-draining here too, so repeated reads
11028    /// are side-effect free.
11029    fn proxy_counter(key: &str) -> i64 {
11030        use sozu_command::proto::command::filtered_metrics::Inner;
11031        crate::metrics::METRICS.with(|metrics| {
11032            metrics
11033                .borrow_mut()
11034                .dump_local_proxy_metrics()
11035                .get(key)
11036                .and_then(|fm| fm.inner.as_ref())
11037                .and_then(|inner| match inner {
11038                    Inner::Count(v) => Some(*v),
11039                    _ => None,
11040                })
11041                .unwrap_or(0)
11042        })
11043    }
11044
11045    /// A counter the core queues is recorded when the entry point that
11046    /// produced it RETURNS — not whenever the connection is eventually
11047    /// dropped.
11048    ///
11049    /// This is the property the gauges did not need. A gauge delta recorded
11050    /// late still sums to the same value, so slice one could be correct with a
11051    /// drain only in `settled` and `Drop`. A counter recorded late is a
11052    /// counter attributed to the wrong minute: `LocalDrain` is cumulative and
11053    /// scraped periodically, so deferring one to connection teardown moves it
11054    /// past however many scrapes the connection outlives. `Drop` remains the
11055    /// backstop that stops it being *lost*; this pins that the backstop is not
11056    /// where it normally happens.
11057    ///
11058    /// `graceful_goaway` is the cheapest real producer to drive: one public
11059    /// call on a fresh connection reaches `ConnectionH2::send_initial_goaway`,
11060    /// which queues both `MetricEvent::GoAwaySent` and
11061    /// `MetricEvent::GoAwayFrameSent`.
11062    ///
11063    /// To SEE THIS RED: delete `self.record_pending_metrics();` from
11064    /// `H2Shell::settled`. Both assertions before `drop` fail with
11065    /// `left: 0, right: 1` — the counters sitting in the core's queue,
11066    /// arriving only when the connection is torn down.
11067    #[test]
11068    fn a_queued_counter_is_recorded_when_its_entry_point_returns() {
11069        let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
11070        let sent_before = proxy_counter("h2.goaway.sent.no_error");
11071        let frames_before = proxy_counter(names::h2::FRAMES_TX_GOAWAY);
11072
11073        let mut connection =
11074            connection_from_source(&mut PoolBufferSource::new(Rc::downgrade(&pool)))
11075                .expect("a pool with free buffers must yield an H2 connection");
11076
11077        connection.graceful_goaway(Instant::now());
11078
11079        assert_eq!(
11080            proxy_counter("h2.goaway.sent.no_error") - sent_before,
11081            1,
11082            "the GOAWAY decision must reach METRICS when `graceful_goaway` \
11083             returns, not at teardown"
11084        );
11085        assert_eq!(
11086            proxy_counter(names::h2::FRAMES_TX_GOAWAY) - frames_before,
11087            1,
11088            "so must the frame the serializer actually wrote"
11089        );
11090    }
11091
11092    /// One `MetricEvent::RstStreamSent` records BOTH counters its two macro
11093    /// lines used to record: the frame-type total and the per-error-code
11094    /// breakdown.
11095    ///
11096    /// Folding two macro lines into one event is only safe where both were
11097    /// unconditional and adjacent, which is why the GOAWAY pair was NOT folded
11098    /// — `h2.frames.tx.goaway` sits inside `serializer::gen_goaway`'s `Ok`
11099    /// arm while `h2.goaway.sent.*` fires before it, so one event would record
11100    /// a frame on the wire that a serialisation failure never wrote. The RST
11101    /// pair has no such gap, and this pins that folding it kept both counters.
11102    ///
11103    /// To SEE THIS RED: delete either line from the
11104    /// `MetricEvent::RstStreamSent` arm of `record_metric`. The surviving
11105    /// assertion passes and the other fails with `left: 0, right: 1` — half a
11106    /// merge, which is exactly what a merged pair can silently become.
11107    #[test]
11108    fn one_rst_stream_sent_event_records_both_of_its_counters() {
11109        let frames_before = proxy_counter(names::h2::FRAMES_TX_RST_STREAM);
11110        let by_code_before = proxy_counter(metric_for_rst_stream_sent(H2Error::Cancel));
11111
11112        record_metric(MetricEvent::RstStreamSent(H2Error::Cancel));
11113
11114        assert_eq!(
11115            proxy_counter(names::h2::FRAMES_TX_RST_STREAM) - frames_before,
11116            1,
11117            "the frame-type total must still tick"
11118        );
11119        assert_eq!(
11120            proxy_counter(metric_for_rst_stream_sent(H2Error::Cancel)) - by_code_before,
11121            1,
11122            "and so must the per-error-code breakdown"
11123        );
11124    }
11125
11126    // ── Clock hygiene: every deadline reads `ConnectionH2::now` ──────────
11127    //
11128    // `Mux` is the only clock sampler; the H2 core reads the snapshot it
11129    // mirrors into `ConnectionH2::now`. A reintroduced `Instant::now()` in
11130    // the core would still "work" in production — it would just be reading a
11131    // second clock — so these two tests advance ONLY the connection's
11132    // snapshot and prove the decision moves with it. Neither sleeps.
11133
11134    // ── TLS backpressure: the GoAway truncation vector (#1454) ──────────
11135    //
11136    // Until this harness existed, NO test anywhere instantiated a
11137    // `ConnectionH2` over a handler whose `socket_wants_write()` could return
11138    // `true`. `mio::net::TcpStream` and `SessionTcpStream` both take the
11139    // trait's `false` default and only `FrontRustls` overrides it, so every
11140    // branch that asks "does rustls still hold records?" was statically dead
11141    // in the test suite — including the one whose own comment calls it the
11142    // primary truncation vector. These are the first tests that branch class
11143    // has ever had against a handler that answers `true`.
11144
11145    /// A `SocketHandler` that models rustls-over-a-blocked-kernel: it holds
11146    /// `pending` records, and each empty-buffer flush drains `drain_per_flush`
11147    /// of them. `drain_per_flush = 0` is a kernel that accepts nothing, which
11148    /// is the backpressure case; a positive value is a kernel that takes them.
11149    ///
11150    /// Modelling the state rather than scripting an answer sequence is
11151    /// deliberate: the close path queries `socket_wants_write()` a number of
11152    /// times that depends on which branches it takes, so a positional script
11153    /// would pin the query COUNT instead of the behaviour and would have to be
11154    /// rewritten by anyone who added a query.
11155    struct BackpressuredTlsSocket {
11156        stream: mio::net::TcpStream,
11157        pending: std::cell::Cell<usize>,
11158        drain_per_flush: usize,
11159        flushes: std::cell::Cell<usize>,
11160        /// An empty-buffer flush that leaves records behind answers
11161        /// `WouldBlock`, as `FrontRustls` does when the kernel refused them,
11162        /// instead of `Continue`. Off by default, so every test written
11163        /// before this field is byte-for-byte unaffected.
11164        refuse_flush: bool,
11165        /// Scripted `(size, status)` answers for `socket_write_vectored`,
11166        /// consumed front to back. An EMPTY script — the default, and what
11167        /// every test written before this field had — delegates to the real
11168        /// loopback socket, so those tests are byte-for-byte unaffected.
11169        ///
11170        /// The record model above cannot express the pair this scripts.
11171        /// `pending`/`drain_per_flush` answer "does rustls still hold
11172        /// records?"; they say nothing about what a *vectored* write
11173        /// reported, because that method delegated to the kernel and the
11174        /// kernel only ever answers `(0, WouldBlock)` when it blocks.
11175        /// `FrontRustls` does not: `socket_write_vectored` accumulates
11176        /// `buffered_size` from `session.writer().write(..)` — plaintext
11177        /// rustls took off the caller's hands — and separately sets
11178        /// `can_write = false` when `write_tls` hits `WouldBlock` against the
11179        /// kernel (`socket.rs`). The two are independent, so
11180        /// `(size > 0, WouldBlock)` is a shape ONLY the TLS handler returns.
11181        /// That is why the script lives on this fixture rather than on a
11182        /// second one: it is the same handler answering the same question the
11183        /// record model already answers, one layer down.
11184        ///
11185        /// Each `size` is a CAP, clamped to what the gather actually offered:
11186        /// a scripted count above the offer would trip the write shell's own
11187        /// `debug_assert!(size <= offered)`, and reddening through a
11188        /// production assertion is the tree noticing rather than the test.
11189        vectored_script: std::collections::VecDeque<(usize, SocketResult)>,
11190        /// Scripted `(size, status)` answers for a NON-EMPTY `socket_write`,
11191        /// consumed front to back exactly like `vectored_script`. An empty
11192        /// script — the default — delegates to the real loopback socket, so
11193        /// every test written before this field is byte-for-byte unaffected.
11194        ///
11195        /// The record model cannot express this either. `pending` /
11196        /// `drain_per_flush` answer "does rustls still hold records?", which
11197        /// is the EMPTY-buffer flush question; this one is "did the kernel
11198        /// take the bytes `flush_output_to_socket` handed it?". A loopback
11199        /// socket with default buffers always takes 13 bytes of
11200        /// WINDOW_UPDATE, so the stalled branch of the control-frame drains
11201        /// is unreachable without scripting the answer.
11202        ///
11203        /// Each `size` is a CAP, clamped to what the caller actually offered:
11204        /// `flush_output_to_socket` feeds the answer straight to
11205        /// `H2Output::consume`, which asserts it never passes the queue.
11206        write_script: std::collections::VecDeque<(usize, SocketResult)>,
11207        /// How many times `socket_write_vectored` was called, whether the
11208        /// answer came from the script or from the delegated loopback socket.
11209        ///
11210        /// Counting BOTH is what makes this a falsifiable witness. Counting
11211        /// only the scripted answers bounds the total by the script's length,
11212        /// so an assertion that the loop did NOT go round again could only
11213        /// ever deviate downwards and could never fail — and the extra
11214        /// delegated rounds are exactly what such an assertion has to be able
11215        /// to see.
11216        vectored_calls: usize,
11217        /// How many NON-EMPTY `socket_write` calls reached this handler. On
11218        /// `FrontRustls` each one ends in its own `write_tls`, so each one is
11219        /// one `writev(2)` on the wire; an empty-buffer flush is counted by
11220        /// `flushes` instead.
11221        writes: usize,
11222    }
11223
11224    impl BackpressuredTlsSocket {
11225        fn new(stream: mio::net::TcpStream, pending: usize, drain_per_flush: usize) -> Self {
11226            Self {
11227                stream,
11228                pending: std::cell::Cell::new(pending),
11229                drain_per_flush,
11230                flushes: std::cell::Cell::new(0),
11231                refuse_flush: false,
11232                vectored_script: std::collections::VecDeque::new(),
11233                write_script: std::collections::VecDeque::new(),
11234                vectored_calls: 0,
11235                writes: 0,
11236            }
11237        }
11238    }
11239
11240    impl SocketHandler for BackpressuredTlsSocket {
11241        fn socket_read(&mut self, buf: &mut [u8]) -> (usize, SocketResult) {
11242            self.stream.socket_read(buf)
11243        }
11244
11245        fn socket_write(&mut self, buf: &[u8]) -> (usize, SocketResult) {
11246            if buf.is_empty() {
11247                self.flushes.set(self.flushes.get() + 1);
11248                let drained = self.drain_per_flush.min(self.pending.get());
11249                self.pending.set(self.pending.get() - drained);
11250                if self.refuse_flush && self.pending.get() > 0 {
11251                    return (0, SocketResult::WouldBlock);
11252                }
11253                return (0, SocketResult::Continue);
11254            }
11255            self.writes += 1;
11256            match self.write_script.pop_front() {
11257                Some((cap, status)) => (cap.min(buf.len()), status),
11258                None => self.stream.socket_write(buf),
11259            }
11260        }
11261
11262        fn socket_write_vectored(&mut self, bufs: &[IoSlice]) -> (usize, SocketResult) {
11263            self.vectored_calls += 1;
11264            match self.vectored_script.pop_front() {
11265                Some((cap, status)) => {
11266                    let offered: usize = bufs.iter().map(|slice| slice.len()).sum();
11267                    (cap.min(offered), status)
11268                }
11269                None => self.stream.socket_write_vectored(bufs),
11270            }
11271        }
11272
11273        /// The override that makes this harness worth having.
11274        fn socket_wants_write(&self) -> bool {
11275            self.pending.get() > 0
11276        }
11277
11278        fn socket_ref(&self) -> &mio::net::TcpStream {
11279            &self.stream
11280        }
11281
11282        fn socket_mut(&mut self) -> &mut mio::net::TcpStream {
11283            &mut self.stream
11284        }
11285
11286        fn peer_addr(&self) -> Option<std::net::SocketAddr> {
11287            mio::net::TcpStream::peer_addr(&self.stream).ok()
11288        }
11289
11290        fn protocol(&self) -> crate::socket::TransportProtocol {
11291            crate::socket::TransportProtocol::Tls1_3
11292        }
11293
11294        fn read_error(&self) {}
11295
11296        fn write_error(&self) {}
11297    }
11298
11299    /// `state` is a parameter rather than a second copy of this fixture: the
11300    /// close decision reads it as `H2State::GoAway` and the write-pass
11301    /// finalization reads it as `H2State::Header`, and both need the same
11302    /// handler underneath.
11303    fn connection_with_backpressure(
11304        pool: &Rc<RefCell<Pool>>,
11305        pending: usize,
11306        drain_per_flush: usize,
11307        state: H2State,
11308    ) -> (H2Shell<BackpressuredTlsSocket>, std::net::TcpStream) {
11309        let (socket, peer) = connected_socket();
11310        let mut connection = H2Shell::new(
11311            Ulid::generate(),
11312            BackpressuredTlsSocket::new(socket, pending, drain_per_flush),
11313            Position::Server,
11314            &mut PoolBufferSource::new(Rc::downgrade(pool)),
11315            H2FloodConfig::default(),
11316            H2ConnectionConfig::default(),
11317            Duration::from_secs(30),
11318            None,
11319            Duration::from_secs(30),
11320            Some((H2StreamId::Zero, CLIENT_PREFACE_SIZE)),
11321            Ready::WRITABLE | Ready::HUP | Ready::ERROR,
11322        )
11323        .expect("a pool with free buffers must yield an H2 connection");
11324        connection.core.state = state;
11325        (connection, peer)
11326    }
11327
11328    /// Records that survive the flush hold the connection open.
11329    ///
11330    /// This is the branch whose own comment calls it the primary truncation
11331    /// vector: closing here sends FIN and destroys the records rustls is still
11332    /// holding, which the client reads as a truncated response.
11333    ///
11334    /// TO SEE THIS RED: delete the `self.ensure_tls_flushed(tls_wants_write);`
11335    /// call in the `CloseAction::ReArmAndContinue` arm of
11336    /// `ConnectionH2::dispatch_writable_state_after_flush`, which is where
11337    /// `H2Shell::writable`'s `H2State::GoAway` branch settles. The WRITABLE
11338    /// *event* bit is then never re-signalled and this test fails on its OWN
11339    /// assertion, `the WRITABLE event must be re-signalled so the event loop
11340    /// retries the flush`. The recipe deliberately does not swap
11341    /// `TlsFlushPhase::AfterFlush` for `BeforeFlush`: that reddens through the
11342    /// production `unreachable!`, which is someone else's assertion and would
11343    /// pass whatever this test claimed.
11344    #[test]
11345    fn a_flush_that_does_not_drain_keeps_the_connection_open() {
11346        let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
11347        // A kernel that accepts nothing: every flush leaves the records.
11348        let (mut connection, _peer) = connection_with_backpressure(&pool, 2, 0, H2State::GoAway);
11349        let mut context = test_context(&pool);
11350        let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
11351
11352        // Premise: the handler really does report buffered records. Without
11353        // this the assertions below would pass against a handler taking the
11354        // trait's `false` default, which is exactly the blind spot #1454 names.
11355        assert!(
11356            connection.socket.socket_wants_write(),
11357            "premise: this harness must report buffered TLS records"
11358        );
11359
11360        let result = connection.writable(&mut context, EndpointClient(&mut router));
11361
11362        assert!(
11363            matches!(result, MuxResult::Continue),
11364            "records still buffered: the session must stay open, got {result:?}"
11365        );
11366        // `Continue` alone does not discriminate: `force_disconnect`'s server
11367        // arm also returns it for a live peer with records pending. The re-arm
11368        // path returns from the GoAway arm BEFORE reaching `force_disconnect`,
11369        // so the state is still `GoAway`; the fall-through would have gone
11370        // through `force_disconnect`, which sets `H2State::Error` first.
11371        assert!(
11372            matches!(connection.core.state, H2State::GoAway),
11373            "the GoAway arm must re-arm and return, not fall through to \
11374             force_disconnect, got {:?}",
11375            connection.core.state
11376        );
11377        // Not `readiness.interest`: that bit is this fixture's own argument to
11378        // `ConnectionH2::new` and nothing on this path clears it, so asserting
11379        // on it would assert on the harness. The edge-triggered re-arm is the
11380        // EVENT bit, set by `ensure_tls_flushed` -> `signal_pending_write`.
11381        assert!(
11382            connection.core.readiness.event.is_writable(),
11383            "the WRITABLE event must be re-signalled so the event loop retries \
11384             the flush, got {:?}",
11385            connection.core.readiness
11386        );
11387        // Not `socket_wants_write()`: with `drain_per_flush = 0` that can never
11388        // change, so it is a tautology of the harness no production edit can
11389        // falsify. The flush COUNT is falsifiable — a GoAway arm that skipped
11390        // its own flush would leave it at 1.
11391        assert!(
11392            connection.socket.flushes.get() >= 2,
11393            "the preamble and the GoAway arm must each attempt a flush, got {}",
11394            connection.socket.flushes.get()
11395        );
11396    }
11397
11398    /// A flush the kernel accepts closes within ONE `writable()` call.
11399    ///
11400    /// This is the tick-count assertion. The pre-image asked its second
11401    /// question inline, immediately after the flush; a split that returned
11402    /// `Continue` and waited to be called again would still deliver the bytes,
11403    /// but would double the latency of every close under backpressure and
11404    /// would strand the connection against a peer that never re-arms.
11405    ///
11406    /// TO SEE THIS RED: in `H2Shell::writable`, add
11407    /// `return MuxResult::Continue;` immediately after the
11408    /// `self.flush_tls_records();` inside the `H2WritableStateTarget::Flush`
11409    /// arm — the flush the `H2State::GoAway` branch asks for, performed and
11410    /// then deferred on. The first call then
11411    /// returns `Continue` and the final assertion fails with `a drained flush
11412    /// must reach the disconnect in the SAME writable call`. Measured: `1115
11413    /// passed; 2 failed`, the sibling being
11414    /// `a_flush_that_does_not_drain_keeps_the_connection_open`, which never
11415    /// reaches its own post-flush answer under the deferral either. Dropping
11416    /// the flush as well would redden the premise at the top of the test
11417    /// instead, with a different message.
11418    #[test]
11419    fn a_flush_that_succeeds_closes_within_one_writable_call() {
11420        let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
11421        // Two records, one drained per flush: the preamble takes one and the
11422        // GoAway arm's own flush takes the other, so the post-flush query is
11423        // the first one that can answer `false`.
11424        let (mut connection, _peer) = connection_with_backpressure(&pool, 2, 1, H2State::GoAway);
11425        let mut context = test_context(&pool);
11426        let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
11427
11428        assert!(
11429            connection.socket.socket_wants_write(),
11430            "premise: this harness must report buffered TLS records"
11431        );
11432
11433        let result = connection.writable(&mut context, EndpointClient(&mut router));
11434
11435        assert!(
11436            !connection.socket.socket_wants_write(),
11437            "premise: both flushes landed, so nothing is pending any more"
11438        );
11439        assert!(
11440            connection.socket.flushes.get() >= 2,
11441            "the preamble and the GoAway arm must each attempt a flush, got {}",
11442            connection.socket.flushes.get()
11443        );
11444        assert!(
11445            !matches!(result, MuxResult::Continue),
11446            "a drained flush must reach the disconnect in the SAME writable \
11447             call, not defer it to another tick: got {result:?}"
11448        );
11449    }
11450
11451    /// A GoAway flush the kernel refuses keeps the connection open WITHOUT
11452    /// re-raising WRITABLE: the kernel owes the socket its next edge, and a
11453    /// synthetic one made `Mux::ready_inner` repeat the refused flush on
11454    /// every inner iteration until `MAX_LOOP_ITERATIONS`
11455    /// (sozu-proxy/sozu#1788). Once the kernel drains, the next writable
11456    /// pass flushes the records and reaches the disconnect, so no record is
11457    /// lost on the way to the close.
11458    ///
11459    /// TO SEE THIS RED: drop `&& !self.kernel_refused_write` from
11460    /// `ConnectionH2::ensure_tls_flushed`. The refused pass then re-raises
11461    /// the WRITABLE event.
11462    #[test]
11463    fn a_refused_goaway_flush_waits_for_the_kernel_edge_then_closes() {
11464        let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
11465        let (mut connection, _peer) = connection_with_backpressure(&pool, 2, 0, H2State::GoAway);
11466        connection.socket.refuse_flush = true;
11467        let mut context = test_context(&pool);
11468        let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
11469        connection.core.readiness.event.insert(Ready::WRITABLE);
11470
11471        let result = connection.writable(&mut context, EndpointClient(&mut router));
11472
11473        assert!(
11474            matches!(result, MuxResult::Continue),
11475            "records still buffered: the session must stay open, got {result:?}"
11476        );
11477        assert!(
11478            matches!(connection.core.state, H2State::GoAway),
11479            "the GoAway arm must keep the connection, got {:?}",
11480            connection.core.state
11481        );
11482        assert!(
11483            connection.core.readiness.interest.is_writable(),
11484            "WRITABLE interest must stay so the kernel edge is delivered"
11485        );
11486        assert!(
11487            !connection.core.readiness.event.is_writable(),
11488            "a refused flush must leave WRITABLE to the kernel edge, got {:?}",
11489            connection.core.readiness
11490        );
11491
11492        // The peer reads: the kernel takes the records and raises its edge.
11493        connection.socket.drain_per_flush = 2;
11494        connection.core.readiness.event.insert(Ready::WRITABLE);
11495        let result = connection.writable(&mut context, EndpointClient(&mut router));
11496
11497        assert!(
11498            !connection.socket.socket_wants_write(),
11499            "the kernel edge must flush the pending records"
11500        );
11501        assert!(
11502            !matches!(result, MuxResult::Continue),
11503            "a drained GoAway flush must reach the disconnect, got {result:?}"
11504        );
11505    }
11506
11507    // ── The write pass's own flush triple (`finalize_write`) ────────────
11508    //
11509    // `ConnectionH2::finalize_write` ends every write pass with the same
11510    // query / flush / query shape the close sites use, and its decision now
11511    // lives beside theirs in `h2_close::finalize_action`. The two tests below
11512    // are the CALLER half — that `h2.rs` performs the step each answer names
11513    // and wires the inputs to the right parameters. The answers themselves are
11514    // enumerated exhaustively in `h2_close`'s own tables, which no socket can
11515    // reach: `SkipFlush`, `Parked` and `RetainPendingBack` are covered there
11516    // only, because reaching them through `writable()` needs a stream carrying
11517    // response bytes through the scheduler and none of this module's fixtures
11518    // builds one. Same boundary `h2_close`'s `force_disconnect` re-arm branch
11519    // already sits on.
11520
11521    /// The pass flushes once of its own and re-arms when records survive it.
11522    ///
11523    /// Three records, one drained per flush: `writable`'s preamble takes the
11524    /// first, `finalize_write`'s own flush takes the second, and the third is
11525    /// still pending when the post-flush query runs — so the re-arm branch is
11526    /// reached with the socket genuinely still holding data, not by a fixture
11527    /// that can only answer one way.
11528    ///
11529    /// TO SEE THIS RED, either half independently:
11530    /// (a) answer `H2FinalizeTarget::SkipFlush` from the
11531    ///     `FinalizeAction::Flush` arm of `finalize_write` (the `SkipFlush`
11532    ///     behaviour, which is what a caller that dropped the `socket_write`
11533    ///     input would do for every pass). `write_streams` then performs no
11534    ///     empty-buffer flush of its own and the flush count assertion fails
11535    ///     with `finalize_write must attempt exactly one empty-buffer flush
11536    ///     of its own`, `left: 1, right: 2`.
11537    /// (b) empty the `FinalizeAction::ReArm` arm of
11538    ///     `finalize_write_after_flush`. The event assertion fails with `the
11539    ///     WRITABLE event must be re-signalled`.
11540    /// Measured: `1114 passed; 1 failed` for each half, this test and nothing
11541    /// else. Neither recipe reddens through a production `unreachable!`.
11542    #[test]
11543    fn a_finalized_write_pass_flushes_once_and_re_arms_while_records_survive() {
11544        let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
11545        let (mut connection, _peer) = connection_with_backpressure(&pool, 3, 1, H2State::Header);
11546        let mut context = test_context(&pool);
11547        let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
11548
11549        assert!(
11550            connection.socket.socket_wants_write(),
11551            "premise: this harness must report buffered TLS records"
11552        );
11553        // Not the interest bit: `signal_pending_write` touches `event` only,
11554        // and this fixture hands `Ready::WRITABLE` to `ConnectionH2::new` as
11555        // its INTEREST, so an assertion written against interest would be
11556        // reading the fixture's own argument back and could not see the
11557        // re-arm at all.
11558        assert!(
11559            !connection.core.readiness.event.is_writable(),
11560            "premise: the WRITABLE event must start clear so the re-arm below              is the only thing that can set it, got {:?}",
11561            connection.core.readiness
11562        );
11563
11564        let result = connection.writable(&mut context, EndpointClient(&mut router));
11565
11566        assert!(
11567            matches!(result, MuxResult::Continue),
11568            "a write pass with records still buffered continues, got {result:?}"
11569        );
11570        assert_eq!(
11571            connection.socket.flushes.get(),
11572            2,
11573            "finalize_write must attempt exactly one empty-buffer flush of its              own on top of the preamble's"
11574        );
11575        assert!(
11576            connection.socket.socket_wants_write(),
11577            "premise: one record must survive both flushes, or the post-flush              query could only answer one way"
11578        );
11579        assert!(
11580            connection.core.readiness.event.is_writable(),
11581            "the WRITABLE event must be re-signalled so the event loop retries              the flush, got {:?}",
11582            connection.core.readiness
11583        );
11584    }
11585
11586    /// A pass that owes nothing withdraws `Ready::WRITABLE` interest.
11587    ///
11588    /// This is the other side of the same decision: no TLS records (a plain
11589    /// `mio::net::TcpStream` takes `socket_wants_write()`'s `false` default),
11590    /// no parked `expect_write`, no bytes written, no queued control frame.
11591    /// Forward progress must come from an external trigger, so the connection
11592    /// relinquishes the bit rather than busy-spinning against the dispatcher.
11593    ///
11594    /// TO SEE THIS RED: delete `self.readiness.interest.remove(Ready::WRITABLE);`
11595    /// from the `FinalizeAction::Quiesce` arm of `finalize_write`, leaving its
11596    /// debug narration in place. The final assertion fails with `a pass that
11597    /// owes nothing must relinquish WRITABLE interest`. The interest bit is
11598    /// asserted here rather than the event bit because withdrawal is what this
11599    /// branch does, and it is set by the test rather than by the fixture —
11600    /// `test_h2_connection` arms `READABLE | HUP | ERROR` only.
11601    #[test]
11602    fn a_write_pass_that_owes_nothing_withdraws_writable_interest() {
11603        let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
11604        let (mut connection, _peer) = test_h2_connection(&pool, None);
11605        let mut context = test_context(&pool);
11606        let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
11607
11608        connection.core.state = H2State::Header;
11609        connection.core.readiness.interest.insert(Ready::WRITABLE);
11610
11611        assert!(
11612            !connection.socket.socket_wants_write(),
11613            "premise: a plain TcpStream holds no TLS records, so the readiness              policy is reached at all"
11614        );
11615        assert!(
11616            connection.core.stream_table.expect_write().is_none(),
11617            "premise: no parked partial write, or the policy is suppressed"
11618        );
11619
11620        let result = connection.writable(&mut context, EndpointClient(&mut router));
11621
11622        assert!(
11623            matches!(result, MuxResult::Continue),
11624            "an empty write pass continues, got {result:?}"
11625        );
11626        assert!(
11627            !connection.core.readiness.interest.is_writable(),
11628            "a pass that owes nothing must relinquish WRITABLE interest, got              {:?}",
11629            connection.core.readiness
11630        );
11631    }
11632
11633    // ── The control-frame drains' own edge-triggered re-arm ─────────────
11634    //
11635    // `ConnectionH2::flush_pending_control_frames` queues the WINDOW_UPDATE
11636    // and RST_STREAM frames behind the ordered output and asks for ONE flush
11637    // (#1604). When that flush stalls, `update_readiness_after_write`
11638    // (`mux/mod.rs`) has just REMOVED the WRITABLE event bit, and the
11639    // `Stalled` answer puts it back when the socket still holds records it
11640    // could not hand to the kernel — unless the kernel refused the write
11641    // (`WouldBlock`): it then owes the socket its next edge, and the re-arm
11642    // would only repeat the refused write (sozu-proxy/sozu#1788,
11643    // `a_refused_goaway_flush_waits_for_the_kernel_edge_then_closes`). The
11644    // two tests below therefore stall the flush on a zero-length write the
11645    // kernel did NOT refuse, the case the re-arm still exists for. Before #1604 each of the two stages had a
11646    // stall tail of its own; the two tests below cover the one that remains.
11647    //
11648    // Until the two tests below existed, nothing in the suite reached either
11649    // stalled branch. Measured by replacing each re-arm with a `panic!` and
11650    // running the whole `sozu-lib` suite against `98745bba`: 1092 passed, 0
11651    // failed, neither panic observed. That is what these tests are for —
11652    // `Readiness::signal_pending_write`'s two `debug_assert!`s only guard a
11653    // path some test actually walks, and a stage that spells the re-arm by
11654    // hand carries no assertion at all.
11655    //
11656    // Neither test can see WHICH spelling a stage uses:
11657    // `readiness.event.insert(Ready::WRITABLE)` and `signal_pending_write()`
11658    // are indistinguishable from outside, by construction — `insert` cannot
11659    // clear a neighbouring bit, so the two are the same state transition.
11660    // What they detect is the re-arm going missing, which strands the
11661    // connection: the serialized frame waits in the output queue and no
11662    // further epoll edge is coming for it.
11663
11664    /// The WINDOW_UPDATE drain re-signals WRITABLE when its flush stalls.
11665    ///
11666    /// TO SEE THIS RED: in `ConnectionH2::handle_control_flush`, replace
11667    /// `H2ControlFlushTarget::Stalled` with
11668    /// `H2ControlFlushTarget::Done(MuxResult::Continue)` — the pass still
11669    /// ends on `Continue`, and the only thing lost is the TLS re-arm
11670    /// `H2Shell::writable` performs for `Stalled`. The last assertion fails
11671    /// with `the stalled WINDOW_UPDATE drain must re-signal the WRITABLE
11672    /// event`: `flush_output_to_socket` cleared the bit on the way in and
11673    /// nothing else on this path sets it. Measured 2026-09-27: this test and
11674    /// its RST_STREAM sibling fail, and no other test of the pair's filter.
11675    #[test]
11676    fn a_stalled_window_update_drain_re_signals_the_writable_event() {
11677        let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
11678        // One record rustls still holds and a kernel that accepts nothing, so
11679        // `socket_wants_write()` answers true for the whole test.
11680        let (mut connection, _peer) = connection_with_backpressure(&pool, 1, 0, H2State::Header);
11681        // The output flush stalls on its first round. A zero-length write
11682        // is what `update_readiness_after_write` reads as a stall, and it
11683        // removes the WRITABLE event bit before the drain stage can re-arm.
11684        // `Continue`, not `WouldBlock`: no kernel edge is owed for it.
11685        connection
11686            .socket
11687            .write_script
11688            .push_back((0, SocketResult::Continue));
11689        let mut context = test_context(&pool);
11690        let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
11691
11692        assert!(
11693            connection.socket.socket_wants_write(),
11694            "premise: this harness must report buffered TLS records, or the \
11695             re-arm is gated off and the assertion below could not fail"
11696        );
11697        // Connection-level replenishment: ordinary housekeeping, queued the
11698        // way `handle_frame` queues it.
11699        connection.core.queue_window_update(0, 65_535);
11700        assert!(
11701            connection.core.stream_table.expect_write().is_none(),
11702            "premise: no parked partial write, or the drain stage is skipped"
11703        );
11704
11705        let result = connection.writable(&mut context, EndpointClient(&mut router));
11706
11707        assert!(
11708            matches!(result, MuxResult::Continue),
11709            "a stalled control-frame flush parks the pass and continues, got \
11710             {result:?}"
11711        );
11712        assert!(
11713            !connection.core.output.is_empty(),
11714            "premise: the pass must have taken the STALLED branch of the \
11715             output flush, which leaves the WINDOW_UPDATE queued"
11716        );
11717        assert!(
11718            connection.core.readiness.event.is_writable(),
11719            "the stalled WINDOW_UPDATE drain must re-signal the WRITABLE \
11720             event: under edge-triggered epoll the parked frame has no other \
11721             wake-up, got {:?}",
11722            connection.core.readiness
11723        );
11724    }
11725
11726    /// The RST_STREAM drain re-signals WRITABLE when its flush stalls.
11727    ///
11728    /// The sibling of the WINDOW_UPDATE test above, on the stage beneath it.
11729    /// No WINDOW_UPDATE is queued on purpose: that stage returns early, so
11730    /// queueing one would make this test exercise the wrong drain.
11731    ///
11732    /// TO SEE THIS RED: the `handle_control_flush` mutation described on the
11733    /// WINDOW_UPDATE test above. The last assertion fails with `the stalled
11734    /// RST_STREAM drain must re-signal the WRITABLE event`. Measured
11735    /// 2026-09-27.
11736    #[test]
11737    fn a_stalled_rst_stream_drain_re_signals_the_writable_event() {
11738        let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
11739        let (mut connection, _peer) = connection_with_backpressure(&pool, 1, 0, H2State::Header);
11740        connection
11741            .socket
11742            .write_script
11743            .push_back((0, SocketResult::Continue));
11744        let mut context = test_context(&pool);
11745        let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
11746
11747        assert!(
11748            connection.socket.socket_wants_write(),
11749            "premise: this harness must report buffered TLS records, or the \
11750             re-arm is gated off and the assertion below could not fail"
11751        );
11752        // One RST, well under the pending-queue bound, so the overflow check
11753        // above this stage does not escalate to GOAWAY(ENHANCE_YOUR_CALM) and
11754        // swallow the drain.
11755        assert!(
11756            connection
11757                .core
11758                .enqueue_rst(1, H2Error::Cancel, RstOrigin::Local)
11759                .is_none(),
11760            "premise: a single RST must not trip the flood detector"
11761        );
11762        assert!(
11763            connection
11764                .core
11765                .flow_control
11766                .pending_window_updates_is_empty(),
11767            "premise: no queued WINDOW_UPDATE, or the stage above returns \
11768             first and this test exercises the wrong drain"
11769        );
11770
11771        let result = connection.writable(&mut context, EndpointClient(&mut router));
11772
11773        assert!(
11774            matches!(result, MuxResult::Continue),
11775            "a stalled control-frame flush parks the pass and continues, got \
11776             {result:?}"
11777        );
11778        assert!(
11779            !connection.core.output.is_empty(),
11780            "premise: the pass must have taken the STALLED branch of the \
11781             output flush, which leaves the RST_STREAM queued"
11782        );
11783        assert!(
11784            connection.core.readiness.event.is_writable(),
11785            "the stalled RST_STREAM drain must re-signal the WRITABLE event: \
11786             under edge-triggered epoll the parked frame has no other wake-up, \
11787             got {:?}",
11788            connection.core.readiness
11789        );
11790    }
11791
11792    /// The kernel-refused sibling of the two drain tests above: the output
11793    /// flush stalls on `(0, WouldBlock)`, and the `Stalled` arm must leave
11794    /// WRITABLE to the kernel's next edge instead of re-raising it. A
11795    /// synthetic edge here made `Mux::ready_inner` repeat the refused flush
11796    /// on every inner iteration (sozu-proxy/sozu#1788).
11797    ///
11798    /// TO SEE THIS RED: drop `self.note_write_status(status);` from
11799    /// `ConnectionH2::consume_output_flush`. The refusal is then never
11800    /// recorded and `ensure_tls_flushed` re-raises the event for both drains.
11801    #[test]
11802    fn a_refused_control_frame_drain_leaves_writable_to_the_kernel() {
11803        for drain in ["WINDOW_UPDATE", "RST_STREAM"] {
11804            let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
11805            let (mut connection, _peer) =
11806                connection_with_backpressure(&pool, 1, 0, H2State::Header);
11807            connection
11808                .socket
11809                .write_script
11810                .push_back((0, SocketResult::WouldBlock));
11811            let mut context = test_context(&pool);
11812            let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
11813            if drain == "WINDOW_UPDATE" {
11814                connection.core.queue_window_update(0, 65_535);
11815            } else {
11816                assert!(
11817                    connection
11818                        .core
11819                        .enqueue_rst(1, H2Error::Cancel, RstOrigin::Local)
11820                        .is_none(),
11821                    "premise: a single RST must not trip the flood detector"
11822                );
11823            }
11824            connection.core.readiness.event.insert(Ready::WRITABLE);
11825
11826            let result = connection.writable(&mut context, EndpointClient(&mut router));
11827
11828            assert!(
11829                matches!(result, MuxResult::Continue),
11830                "a refused {drain} drain parks the pass and continues, got {result:?}"
11831            );
11832            assert!(
11833                !connection.core.output.is_empty(),
11834                "premise: the {drain} flush must have stalled, leaving the frame queued"
11835            );
11836            assert!(
11837                connection.socket.socket_wants_write(),
11838                "premise: rustls must still hold records, or the re-arm is gated off"
11839            );
11840            assert!(
11841                !connection.core.readiness.event.is_writable(),
11842                "a {drain} drain the kernel refused must leave WRITABLE to the \
11843                 kernel's next edge, got {:?}",
11844                connection.core.readiness
11845            );
11846        }
11847    }
11848
11849    /// A stream write the kernel refuses, while rustls still holds records,
11850    /// ends the pass without a synthetic WRITABLE. The pass already wrote
11851    /// through `socket_write_vectored`, so `finalize_write` skips its own
11852    /// flush and decides on the refusal `handle_write` recorded: the preamble
11853    /// flush before it answered `Continue`, so without that record the
11854    /// `ReArm` would re-raise the event.
11855    ///
11856    /// TO SEE THIS RED: drop `self.note_write_status(status);` from
11857    /// `ConnectionH2::handle_write`.
11858    #[test]
11859    fn a_refused_stream_write_leaves_writable_to_the_kernel() {
11860        let pool = make_pool_for_invariant_16();
11861        let (mut connection, mut context, gid, _peer) = writable_fixture(
11862            &pool,
11863            &[FIRST_BLOCK, SECOND_BLOCK],
11864            &[(0, SocketResult::WouldBlock)],
11865        );
11866        connection.socket.pending.set(1);
11867        let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
11868        assert_prepare_gate_is_shut(&context, gid);
11869        connection.core.readiness.event.insert(Ready::WRITABLE);
11870
11871        let result = connection.writable(&mut context, EndpointClient(&mut router));
11872
11873        assert!(
11874            matches!(result, MuxResult::Continue),
11875            "a refused stream write continues, got {result:?}"
11876        );
11877        assert_eq!(
11878            connection.socket.vectored_calls, 1,
11879            "premise: the pass must have reached the stream write"
11880        );
11881        assert!(
11882            connection.socket.socket_wants_write(),
11883            "premise: rustls must still hold records"
11884        );
11885        assert!(
11886            !connection.core.readiness.event.is_writable(),
11887            "a stream write the kernel refused must leave WRITABLE to the \
11888             kernel's next edge, got {:?}",
11889            connection.core.readiness
11890        );
11891    }
11892
11893    // ── The vectored write loop: a partial write that reports WouldBlock ──
11894    //
11895    // First, which shape each neighbour targets, because #1454 asks for that
11896    // and the shapes are near-identical. `h2.rs` has FOUR `socket_write(&[])`
11897    // sites. Three are the `query / socket_write(&[]) / query` TRIPLE, where
11898    // the middle call discards both `size` and `status` and only the second
11899    // query says whether the flush landed:
11900    //
11901    //   - `finalize_write`, the BeforeFlush/AfterFlush pair — covered by
11902    //     `a_finalized_write_pass_flushes_once_and_re_arms_while_records_survive`.
11903    //   - the `H2State::GoAway` arm of `dispatch_writable_state`, whose flush
11904    //     `writable` performs for it — covered by
11905    //     `a_flush_that_does_not_drain_keeps_the_connection_open` and
11906    //     `a_flush_that_succeeds_closes_within_one_writable_call`.
11907    //   - `writable`'s preamble paired with the `error_close_action` query of
11908    //     `dispatch_writable_state`'s `(H2State::Error, Position::Server)`
11909    //     arm. The preamble half is asserted by the two GoAway tests above,
11910    //     and #1454's
11911    //     `a_rustls_frontend_in_error_state_stays_open_until_its_records_drain`
11912    //     drives the second query over the production handler — this line
11913    //     said **Uncovered** until that test existed, and it was already
11914    //     wrong before this changeset renamed what it points at.
11915    //
11916    // The fourth, in `flush_output_buffer`, is NOT a triple: it keeps the
11917    // `status` the flush returned instead of re-querying. Also uncovered.
11918    //
11919    // The tests below target none of those. They target **the vectored loop
11920    // the triples bracket** — the pre-image's `while !kawa.out.is_empty()`,
11921    // now `poll_write_target`'s round-again over `H2WritePhase::Flush` — so
11922    // naming a triple for them would be naming the wrong shape.
11923    //
11924    // What had never run against production code: a `socket_write_vectored`
11925    // that moves bytes AND reports `WouldBlock`. `update_readiness` (`mux/mod.rs`)
11926    // classifies a pass as stalled **iff `size == 0`** — `status` only clears
11927    // the event bit — so `size > 0 && status == WouldBlock` is NOT a stall and
11928    // the loop must go round again. A write machine that treated
11929    // `status != Continue` as a pass terminator would drop that second write
11930    // and truncate the response, and would pass every test that existed.
11931    //
11932    // Why no test could see it. `BackpressuredTlsSocket::socket_write_vectored`
11933    // delegated to a real loopback socket, and a kernel that blocks answers
11934    // `(0, WouldBlock)` — never `(size > 0, WouldBlock)`. Only `FrontRustls`
11935    // returns that pair, and it does so structurally: `buffered_size` counts
11936    // plaintext `session.writer().write(..)` accepted, while `can_write` goes
11937    // false when `write_tls` blocks against the kernel. Two independent
11938    // quantities, one return value.
11939    //
11940    // `h2_transmit`'s `qc_partial_writes_preserve_the_byte_stream` does drive
11941    // partial writes, but through `drive`, a loop written in its own test
11942    // module that calls neither `ConnectionH2::handle_write` nor
11943    // `update_readiness_after_write`; its `WritePlan` carries an accept COUNT
11944    // and no `SocketResult` at all. A mirror cannot constrain the thing it
11945    // mirrors, and that plan cannot express this pair in the first place.
11946
11947    /// What the pre-image's `FlushOutcome` said about one stream's flush,
11948    /// re-derived here because production no longer has the type.
11949    ///
11950    /// The inversion deleted it along with `flush_stream_out`: the flush loop
11951    /// is now the `Transmit`/`handle_write` round trip, whose exit condition
11952    /// is read by `poll_write_target` from `kawa.out` and `pass.stalled`
11953    /// rather than reported as a return value. `Drained` is therefore derived
11954    /// from the queue being empty — the pre-image's own
11955    /// `while !kawa.out.is_empty()` exit condition, verbatim — and NOT from
11956    /// `expect_write` being parked. The distinction matters: the park has its
11957    /// own dedicated tests below, and deriving the outcome from it would make
11958    /// this classification move under edits those tests already cover.
11959    ///
11960    /// **It is therefore the same fact as the `out_is_empty` element of
11961    /// [`drive_flush_stream_out`]'s tuple, and the three tests below assert
11962    /// both.** Stated rather than removed: their bodies are unchanged from
11963    /// before the inversion, which is what makes them evidence that spans it,
11964    /// and the alternatives were measured worse. Deriving this from
11965    /// `expect_write` would couple it to the park, which the stall tests
11966    /// already pin — the double-assertion would become a second test reddening
11967    /// under the `set_expect_write` recipe. Deriving it from the WRITABLE
11968    /// event bit cannot work either: `update_readiness` clears that bit on
11969    /// every non-`Continue` round, so a drained two-round pass and a stalled
11970    /// one leave it identical.
11971    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
11972    enum FlushOutcome {
11973        /// All queued bytes were drained to the socket.
11974        Drained,
11975        /// The socket blocked before the queue was drained.
11976        Stalled,
11977    }
11978
11979    /// Drive one write pass over `blocks` with `script` as the socket's
11980    /// answers, returning what the flush did.
11981    ///
11982    /// Retargeted by the poll/handle inversion. There is no associated
11983    /// function left to call: the loop this drives is spread across
11984    /// `poll_write_target`'s round-again, the shell's gather / write / confirm
11985    /// and `handle_write`, and the smallest production seam that contains all
11986    /// three is `write_streams`. It is called directly rather than through
11987    /// `writable()` so that the preamble's pending-control-frame flush and TLS
11988    /// flush stay out of the measurement; `pending = 0` keeps
11989    /// `socket_wants_write()` false for the whole pass, and nothing below
11990    /// asserts on time, so skipping `writable`'s `self.now = context.now` is
11991    /// inert.
11992    ///
11993    /// What is unchanged is the point of the harness: `vectored_script` still
11994    /// lets the `(size, status)` pair be CHOSEN rather than negotiated with a
11995    /// kernel, which is the only way `(size > 0, WouldBlock)` is expressible
11996    /// at all.
11997    fn drive_flush_stream_out(
11998        pool: &Rc<RefCell<Pool>>,
11999        blocks: &[&'static [u8]],
12000        script: &[(usize, SocketResult)],
12001    ) -> (FlushOutcome, usize, usize, bool, Readiness) {
12002        let (mut connection, mut context, gid, _peer) = writable_fixture(pool, blocks, script);
12003        let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
12004
12005        connection.write_streams(&mut context, EndpointClient(&mut router));
12006
12007        let out_is_empty = context.streams[gid].back.out.is_empty();
12008        (
12009            if out_is_empty {
12010                FlushOutcome::Drained
12011            } else {
12012                FlushOutcome::Stalled
12013            },
12014            context.streams[gid].metrics.bout,
12015            connection.socket.vectored_calls,
12016            out_is_empty,
12017            connection.core.readiness.clone(),
12018        )
12019    }
12020
12021    /// Bytes the two blocks below carry, and the whole response as far as
12022    /// this stream is concerned: ONE DATA frame on stream 1, split across two
12023    /// blocks, 22 + 21 bytes. A stream's `kawa.out` only ever holds whole H2
12024    /// frames, and since #1604 the write pass relies on it to find where the
12025    /// frame a partial write cut ends (`h2_transmit::frame_tail`).
12026    const FIRST_BLOCK: &[u8] = b"\x00\x00\x22\x00\x00\x00\x00\x00\x01HTTP/2 respon";
12027    const SECOND_BLOCK: &[u8] = b" and its continuation";
12028    const TOTAL_QUEUED: usize = FIRST_BLOCK.len() + SECOND_BLOCK.len();
12029
12030    /// A write that moved bytes but reported `WouldBlock` does NOT end the
12031    /// pass: the loop goes round again and delivers the rest.
12032    ///
12033    /// This is the truncation vector of #1454 at the vectored loop — not at
12034    /// any of the three `socket_wants_write()` triples. `FrontRustls` returns
12035    /// `(buffered_size, WouldBlock)` with `buffered_size > 0` whenever rustls
12036    /// took plaintext off our hands while the kernel was full, and
12037    /// `update_readiness` deliberately calls that NOT stalled. A machine that
12038    /// terminated the pass on `status != Continue` would leave
12039    /// `SECOND_BLOCK` queued and send the response short.
12040    ///
12041    /// TO SEE THIS RED: in `handle_write`, change
12042    /// `pass.stalled = update_readiness_after_write(size, status, &mut self.readiness);`
12043    /// to
12044    /// `pass.stalled = update_readiness_after_write(size, status, &mut self.readiness)
12045    ///      || !matches!(status, SocketResult::Continue);`
12046    /// — the pass-terminator shape the `write_streams` inversion could
12047    /// introduce, and the reason `SocketResult` is confined to this one
12048    /// statement and this one parameter. Measured on that mutation: this test
12049    /// fails on its own first assertion, `a vectored write that moved bytes
12050    /// but reported WouldBlock must not end the pass`, with
12051    /// `left: 5, right: 43`. It is one of THREE failures, the siblings being
12052    /// `a_partial_write_reporting_would_block_continues_the_pass_through_writable`
12053    /// and `a_yielded_incremental_stream_is_prepared_once_per_pass`; every
12054    /// other test in the suite passes, including both controls below. No
12055    /// production `debug_assert!` fires: every scripted size is clamped to the
12056    /// gather's offer.
12057    #[test]
12058    fn a_partial_write_reporting_would_block_continues_the_pass() {
12059        let pool = make_pool_for_invariant_16();
12060        // Round 1 accepts 5 of the 43 offered and reports WouldBlock; round 2
12061        // takes everything still queued and reports Continue.
12062        // The event bit is deliberately not read here: `update_readiness`
12063        // removes WRITABLE on the WouldBlock round and the Continue round does
12064        // not put it back, so its final value is identical under the correct
12065        // and the broken machine. An assertion on it could not fail.
12066        let (outcome, bytes_written, vectored_calls, out_is_empty, _readiness) =
12067            drive_flush_stream_out(
12068                &pool,
12069                &[FIRST_BLOCK, SECOND_BLOCK],
12070                &[
12071                    (5, SocketResult::WouldBlock),
12072                    (TOTAL_QUEUED, SocketResult::Continue),
12073                ],
12074            );
12075
12076        assert_eq!(
12077            bytes_written, TOTAL_QUEUED,
12078            "a vectored write that moved bytes but reported WouldBlock must \
12079             not end the pass: the loop must go round again and deliver the \
12080             remaining bytes, or the response is truncated"
12081        );
12082        assert_eq!(
12083            vectored_calls, 2,
12084            "the loop must issue a SECOND socket_write_vectored after the \
12085             partial write, not settle for the first"
12086        );
12087        assert!(
12088            out_is_empty,
12089            "every queued block must be consumed once the pass drains"
12090        );
12091        assert!(
12092            matches!(outcome, FlushOutcome::Drained),
12093            "a queue emptied within the pass is Drained, got {outcome:?}"
12094        );
12095    }
12096
12097    /// A write the socket takes whole, in one round, reporting `Continue`.
12098    ///
12099    /// **This test does not discriminate, and that is its job.** It is the
12100    /// `status == Continue` path — the one every existing test already takes,
12101    /// because a real loopback socket with room answers exactly this. It
12102    /// passes against the correct loop AND against the pass-terminator
12103    /// mutation in the test above, since a terminator keyed on
12104    /// `status != Continue` never fires here. Kept as the standing
12105    /// demonstration that coverage of this shape proves nothing about the
12106    /// truncation vector: the suite was full of it and #1454 was open anyway.
12107    #[test]
12108    fn a_fully_accepted_write_drains_in_one_round() {
12109        let pool = make_pool_for_invariant_16();
12110        let (outcome, bytes_written, vectored_calls, out_is_empty, _readiness) =
12111            drive_flush_stream_out(
12112                &pool,
12113                &[FIRST_BLOCK, SECOND_BLOCK],
12114                &[(TOTAL_QUEUED, SocketResult::Continue)],
12115            );
12116
12117        assert_eq!(
12118            bytes_written, TOTAL_QUEUED,
12119            "a socket that takes the whole offer delivers the whole queue"
12120        );
12121        assert_eq!(
12122            vectored_calls, 1,
12123            "nothing is left to write, so the loop must not go round again"
12124        );
12125        assert!(out_is_empty, "the queue must be empty after a full accept");
12126        assert!(
12127            matches!(outcome, FlushOutcome::Drained),
12128            "a fully accepted offer is Drained, got {outcome:?}"
12129        );
12130    }
12131
12132    /// A write that moves NO bytes stalls the pass and drops WRITABLE.
12133    ///
12134    /// The other side of `update_readiness`'s single decision: `size == 0` is
12135    /// the stall, whatever the status. Together with the partial-write test
12136    /// above this pins that the discriminator is the SIZE and not the status —
12137    /// both rounds there reported a non-`Continue` status, and only this one
12138    /// stops.
12139    ///
12140    /// Like `a_fully_accepted_write_drains_in_one_round`, this passes under
12141    /// the pass-terminator mutation too: `(0, WouldBlock)` stalls either way.
12142    /// The size arm is the covered one; the partial arm was not.
12143    ///
12144    /// TO SEE THIS RED: in `update_readiness` (`mux/mod.rs`), change the
12145    /// `else` arm's trailing `true` to `false`, so no size ever reports a
12146    /// stall. Measured: the first assertion fails with `a write that moved no
12147    /// bytes must stall the pass, got Drained`. It does not hang — the script
12148    /// here is a single round, so the loop re-enters, finds the script spent,
12149    /// delegates to the live loopback socket and drains.
12150    #[test]
12151    fn a_write_that_moves_no_bytes_stalls_the_pass() {
12152        let pool = make_pool_for_invariant_16();
12153        let (outcome, bytes_written, vectored_calls, out_is_empty, readiness) =
12154            drive_flush_stream_out(
12155                &pool,
12156                &[FIRST_BLOCK, SECOND_BLOCK],
12157                &[(0, SocketResult::WouldBlock)],
12158            );
12159
12160        assert!(
12161            matches!(outcome, FlushOutcome::Stalled),
12162            "a write that moved no bytes must stall the pass, got {outcome:?}"
12163        );
12164        assert_eq!(
12165            vectored_calls, 1,
12166            "a stalled pass must not retry the socket within the same pass"
12167        );
12168        assert_eq!(bytes_written, 0, "a stalled write delivered nothing");
12169        assert!(
12170            !out_is_empty,
12171            "the undelivered blocks must stay queued for the next pass, or \
12172             they are lost"
12173        );
12174        assert!(
12175            !readiness.event.is_writable(),
12176            "a stalled pass must clear the WRITABLE event so the pass is not \
12177             re-entered before the socket says it can take more, got {readiness:?}"
12178        );
12179    }
12180
12181    // ── The two write-path sites `writable()` had never reached ──────────
12182    //
12183    // A stalled flush has ONE producer — the `pass.stalled` assignment in
12184    // `handle_write`, `FlushOutcome::Stalled` before the inversion — and
12185    // exactly two consumers, and both consumers were dead code as far as the
12186    // suite was concerned:
12187    //
12188    //   - the resume path's `H2WriteTarget::Done(MuxResult::Continue)`, taken
12189    //     in `H2WritePhase::Resume` for the stream `expect_write` parked, whose
12190    //     transmits carry `debug_site = 2`;
12191    //   - `H2WritePhase::Flush`'s
12192    //     `set_expect_write(Some(H2StreamId::Other { .. }))` followed by the
12193    //     move to `H2WritePhase::End` — `break 'outer` before the inversion.
12194    //
12195    // Measured on `38fe5851`, against the linear pre-image these tests were
12196    // written for: a `panic!` planted at BOTH consumers at once still leaves
12197    // the whole suite green — neither is reached by any test. A `panic!` on
12198    // the `return FlushOutcome::Stalled` they both read fails exactly ONE
12199    // test, `a_write_that_moves_no_bytes_stalls_the_pass` above, which called
12200    // `flush_stream_out` directly and never entered `write_streams` at all.
12201    // A `panic!` on the first line of the resume block leaves the suite green
12202    // as well: `debug_site = 2` had no caller in it. Those measurements are a
12203    // record of the revision named, not a claim about this one — the three
12204    // tests above now reach `write_streams` too, since `flush_stream_out` is
12205    // gone and there is no associated function left to call.
12206    //
12207    // No absolute pass count appears in this block or in any recipe below, on
12208    // purpose. A count of the whole `sozu-lib` suite is falsified by a test
12209    // added anywhere in the crate, and nothing checks it —
12210    // `check_doc_citations.py` reads only `doc/` and `**/LIFECYCLE.md`. The
12211    // failing test NAMES and the `left:`/`right:` values are what survive.
12212    //
12213    // Why the gap existed. A stall needs `size == 0` against a non-empty
12214    // `kawa.out`. The two `BackpressuredTlsSocket` `writable()` tests above
12215    // sit in `H2State::GoAway` and take that arm of `writable`, never
12216    // `write_streams`; the two after them do reach `write_streams`, but with
12217    // an empty stream table, so the scheduler loop visits no stream. Every
12218    // other `writable()` test writes to a live loopback socket with room, and
12219    // a kernel with room never answers 0.
12220    //
12221    // The missing step was never the socket — `vectored_script` already
12222    // scripts any `(size, status)` pair, including ones no kernel produces.
12223    // It was REGISTERING a stream that carries response bytes, so the
12224    // scheduler reaches it through `writable()`. That is what
12225    // `writable_fixture` below adds. It is now what `drive_flush_stream_out`
12226    // builds on too: the difference left between the two is only that this
12227    // group enters through `writable()` and that one through `write_streams`
12228    // directly.
12229
12230    /// The stream id every test below registers. Odd, because a server's peer
12231    /// opens odd-numbered streams (RFC 9113 §5.1.1).
12232    const REGISTERED_STREAM_ID: StreamId = 1;
12233
12234    /// A server connection in `H2State::Header` plus a context holding ONE
12235    /// registered stream whose response buffer already carries `blocks`, with
12236    /// the socket's `(size, status)` answers scripted.
12237    ///
12238    /// `pending = 0` keeps `socket_wants_write()` false for the whole pass, so
12239    /// `writable`'s preamble flush and both of `finalize_write`'s TLS branches
12240    /// stay out of the way: the readiness bits these tests read are decided by
12241    /// the write pass alone.
12242    ///
12243    /// The script is assigned to the field directly: a builder taking `self`
12244    /// by value cannot be applied to a handler already moved into
12245    /// `ConnectionH2::new`.
12246    ///
12247    /// The peer end of the loopback pair is returned for the caller to hold:
12248    /// dropping it would close the connection under the socket being tested.
12249    fn writable_fixture(
12250        pool: &Rc<RefCell<Pool>>,
12251        blocks: &[&'static [u8]],
12252        script: &[(usize, SocketResult)],
12253    ) -> (
12254        H2Shell<BackpressuredTlsSocket>,
12255        Context<TestListener>,
12256        GlobalStreamId,
12257        std::net::TcpStream,
12258    ) {
12259        let (mut connection, peer) = connection_with_backpressure(pool, 0, 0, H2State::Header);
12260        connection.socket.vectored_script = script.iter().copied().collect();
12261        let mut context = test_context(pool);
12262        let gid = context
12263            .create_stream(Ulid::generate(), 1 << 16)
12264            .expect("test context must create a stream");
12265        connection
12266            .core
12267            .stream_table
12268            .register(REGISTERED_STREAM_ID, gid, connection.core.now);
12269        for block in blocks {
12270            context.streams[gid]
12271                .back
12272                .out
12273                .push_back(kawa::OutBlock::Store(kawa::Store::Static(block)));
12274        }
12275        (connection, context, gid, peer)
12276    }
12277
12278    /// A fresh `Stream`'s response buffer is in `ParsingPhase::StatusLine`,
12279    /// which `is_main_phase()` answers `false` for, so the `write_streams`
12280    /// prepare gate stays shut and the pass is the flush and nothing else.
12281    ///
12282    /// Every `writable_fixture` test rests on that, because each one counts
12283    /// either bytes or socket calls and a prepare would add both. Asserted as
12284    /// a premise rather than assumed: if a future `Stream::new` started life
12285    /// in `Body`, those tests would keep passing while measuring something
12286    /// else. `a_yielded_incremental_stream_is_prepared_once_per_pass` below
12287    /// deliberately opens the gate instead, and builds its own fixture.
12288    fn assert_prepare_gate_is_shut(context: &Context<TestListener>, gid: GlobalStreamId) {
12289        let back = &context.streams[gid].back;
12290        assert!(
12291            !back.is_main_phase() && !back.is_terminated() && !back.is_error(),
12292            "premise: the prepare gate must stay shut so this pass is a pure \
12293             flush, got parsing_phase {:?}",
12294            back.parsing_phase
12295        );
12296    }
12297
12298    /// A write that moved bytes but reported `WouldBlock` does not end the
12299    /// pass — driven through `writable()`, over a registered stream.
12300    ///
12301    /// `a_partial_write_reporting_would_block_continues_the_pass` above pins
12302    /// the same contract one layer down, entering at `write_streams` and
12303    /// reading the flush's own outcome. This one pins that the SCHEDULER
12304    /// honours it: the `(size > 0, WouldBlock)` answer has to travel back out
12305    /// through `poll_write_target`'s round-again at `H2WritePhase::Flush`,
12306    /// past the `set_expect_write` park, and through `finalize_write` without
12307    /// anything on the way deciding the pass is over. A write machine that
12308    /// treated `status != Continue` as a pass terminator would park
12309    /// `expect_write`, leave `SECOND_BLOCK` queued, and truncate the response.
12310    ///
12311    /// TO SEE THIS RED: in `handle_write`, change
12312    /// `pass.stalled = update_readiness_after_write(size, status, &mut self.readiness);`
12313    /// to
12314    /// `pass.stalled = update_readiness_after_write(size, status, &mut self.readiness)
12315    ///      || !matches!(status, SocketResult::Continue);`
12316    /// Measured: this test fails on its own first assertion, `every queued
12317    /// byte must reach the socket within the pass`, with `left: 5, right: 43`.
12318    /// That edit also reddens two siblings, so THREE tests fail and not one:
12319    /// `a_partial_write_reporting_would_block_continues_the_pass`, the same
12320    /// contract on the direct path, documenting the same recipe (`left: 5,
12321    /// right: 43`); and
12322    /// `a_yielded_incremental_stream_is_prepared_once_per_pass` below, whose
12323    /// first stream stalls on its `WouldBlock` round so the pass goes to
12324    /// `H2WritePhase::End` before the second stream is ever prepared
12325    /// (`left: 16384, right: 32768`). Three witnesses to one truncation vector, at three different
12326    /// depths — not a duplicate. No production `debug_assert!` fires: every
12327    /// scripted size is clamped to the gather's offer.
12328    #[test]
12329    fn a_partial_write_reporting_would_block_continues_the_pass_through_writable() {
12330        let pool = make_pool_for_invariant_16();
12331        let (mut connection, mut context, gid, _peer) = writable_fixture(
12332            &pool,
12333            &[FIRST_BLOCK, SECOND_BLOCK],
12334            &[
12335                (5, SocketResult::WouldBlock),
12336                (TOTAL_QUEUED, SocketResult::Continue),
12337            ],
12338        );
12339        let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
12340
12341        assert_eq!(
12342            connection.core.stream_table.expect_write(),
12343            None,
12344            "premise: no parked write, so the pass takes the main loop and not \
12345             the resume path"
12346        );
12347        assert_prepare_gate_is_shut(&context, gid);
12348
12349        let result = connection.writable(&mut context, EndpointClient(&mut router));
12350
12351        assert_eq!(
12352            context.streams[gid].metrics.bout, TOTAL_QUEUED,
12353            "every queued byte must reach the socket within the pass: a write \
12354             that moved bytes under WouldBlock is not a stall, so the loop must \
12355             go round again instead of ending the pass short"
12356        );
12357        assert_eq!(
12358            connection.socket.vectored_calls, 2,
12359            "the pass must issue a SECOND socket_write_vectored after the \
12360             partial write, not settle for the first"
12361        );
12362        assert!(
12363            context.streams[gid].back.out.is_empty(),
12364            "every queued block must be consumed once the pass drains"
12365        );
12366        assert_eq!(
12367            connection.core.stream_table.expect_write(),
12368            None,
12369            "a pass that drained must not park expect_write: parking here \
12370             would defer a stream that owes nothing to the next writable tick"
12371        );
12372        assert!(
12373            matches!(result, MuxResult::Continue),
12374            "a drained write pass continues, got {result:?}"
12375        );
12376    }
12377
12378    /// A write pass that transmits allocates nothing once the connection is
12379    /// warm: the vectored descriptors live in `H2Shell::io_slices`, which
12380    /// keeps its capacity from one pass to the next instead of being built
12381    /// and freed by every `write_streams` call.
12382    ///
12383    /// The first pass is the warm-up and may grow the scratch; the second
12384    /// queues the same two blocks again and is the measurement. Both go
12385    /// through the loopback socket (empty script), so the write is a real
12386    /// `writev(2)` and the count covers the whole gather / write / confirm
12387    /// round trip plus `poll_write_target` and `handle_write` around it.
12388    ///
12389    /// TO SEE THIS RED: in `H2Shell::write_streams`, declare
12390    /// `let mut io_slices: Vec<IoSlice<'static>> = Vec::new();` above the
12391    /// loop and hand it to `gather`, the write and `confirm` in place of
12392    /// `self.io_slices`. Measured: this test fails with
12393    /// `left: 2, right: 1` under `cargo test`, and `left: 1, right: 0`
12394    /// under `cargo test --release` — one allocation per transmitting pass.
12395    #[test]
12396    fn a_warm_write_pass_allocates_nothing() {
12397        let pool = make_pool_for_invariant_16();
12398        let (mut connection, mut context, gid, _peer) =
12399            writable_fixture(&pool, &[FIRST_BLOCK, SECOND_BLOCK], &[]);
12400        let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
12401        assert_prepare_gate_is_shut(&context, gid);
12402
12403        connection.write_streams(&mut context, EndpointClient(&mut router));
12404        assert!(
12405            context.streams[gid].back.out.is_empty(),
12406            "premise: the warm-up pass must drain, or the measured pass would \
12407             start from a different state"
12408        );
12409        for block in [FIRST_BLOCK, SECOND_BLOCK] {
12410            context.streams[gid]
12411                .back
12412                .out
12413                .push_back(kawa::OutBlock::Store(kawa::Store::Static(block)));
12414        }
12415        let calls_before = connection.socket.vectored_calls;
12416
12417        let before = crate::test_allocations::allocations();
12418        connection.write_streams(&mut context, EndpointClient(&mut router));
12419        let allocations = crate::test_allocations::allocations() - before;
12420
12421        assert!(
12422            connection.socket.vectored_calls > calls_before,
12423            "premise: the measured pass must reach socket_write_vectored"
12424        );
12425        assert!(
12426            context.streams[gid].back.out.is_empty(),
12427            "premise: the measured pass must drain the stream"
12428        );
12429        // `finalize_write`'s `Quiesce` arm formats a `DebugEvent::Str` under
12430        // `#[cfg(debug_assertions)]` only; a release build does not compile
12431        // it. Everything else the pass does must be allocation-free.
12432        let debug_instrumentation = usize::from(cfg!(debug_assertions));
12433        assert_eq!(
12434            allocations, debug_instrumentation,
12435            "a warm transmitting write pass must not allocate"
12436        );
12437    }
12438
12439    /// A zero-byte write parks `expect_write` on the stalled stream and ends
12440    /// the pass, so the next tick resumes exactly it.
12441    ///
12442    /// This is `poll_write_target`'s stall consumer — the
12443    /// `set_expect_write(Some(write_stream));` plus the move to
12444    /// `H2WritePhase::End` in the `H2WritePhase::Flush` arm, `break 'outer`
12445    /// before the inversion. Without the park, the next `writable()` would
12446    /// re-enter the scheduler from the top and re-run the priority ordering
12447    /// for a stream that is merely waiting on the socket, and `finalize_write`
12448    /// would read `expect_write_parked == false` and fall through to
12449    /// `Quiesce`, stripping `Ready::WRITABLE` from a connection that has bytes
12450    /// to deliver.
12451    ///
12452    /// TO SEE THIS RED: delete the
12453    /// `self.stream_table.set_expect_write(Some(write_stream));` statement
12454    /// from that arm, keeping the `pass.phase = H2WritePhase::End;` that
12455    /// follows it. Measured: this test fails on its own first
12456    /// assertion, `a stalled stream must park expect_write so the next pass
12457    /// resumes it`, with `left: None, right: Some(Other { id: 1, gid: 0 })`,
12458    /// and it is the ONLY failure in the suite.
12459    #[test]
12460    fn a_stalled_stream_parks_expect_write_for_the_next_pass() {
12461        let pool = make_pool_for_invariant_16();
12462        let (mut connection, mut context, gid, _peer) = writable_fixture(
12463            &pool,
12464            &[FIRST_BLOCK, SECOND_BLOCK],
12465            &[(0, SocketResult::WouldBlock)],
12466        );
12467        let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
12468
12469        assert_eq!(
12470            connection.core.stream_table.expect_write(),
12471            None,
12472            "premise: nothing parked on entry, so any park below is this \
12473             pass's doing"
12474        );
12475        assert_prepare_gate_is_shut(&context, gid);
12476
12477        let result = connection.writable(&mut context, EndpointClient(&mut router));
12478
12479        assert_eq!(
12480            connection.core.stream_table.expect_write(),
12481            Some(H2StreamId::Other {
12482                id: REGISTERED_STREAM_ID,
12483                gid
12484            }),
12485            "a stalled stream must park expect_write so the next pass resumes \
12486             it instead of re-running the scheduler over a socket-blocked stream"
12487        );
12488        assert!(
12489            !context.streams[gid].back.out.is_empty(),
12490            "the undelivered blocks must stay queued for the next pass, or \
12491             they are lost"
12492        );
12493        assert_eq!(
12494            connection.socket.vectored_calls, 1,
12495            "a stalled pass must not retry the socket within the same pass"
12496        );
12497        assert_eq!(
12498            context.streams[gid].metrics.bout, 0,
12499            "a stalled write delivered nothing"
12500        );
12501        assert!(
12502            connection.core.readiness.interest.is_writable(),
12503            "a parked pass leaves every readiness bit alone — stripping \
12504             WRITABLE here would strand the parked stream, got {:?}",
12505            connection.core.readiness
12506        );
12507        assert!(
12508            matches!(result, MuxResult::Continue),
12509            "a parked write pass continues, got {result:?}"
12510        );
12511    }
12512
12513    /// A stalled resume returns before the scheduler pass begins.
12514    ///
12515    /// This is the OTHER stall consumer — `H2WritePhase::Resume`'s
12516    /// `if pass.stalled { return H2WriteTarget::Done(MuxResult::Continue); }`.
12517    /// The park it leaves standing is the point: falling through would clear
12518    /// `expect_write` on a stream the socket has just refused, and then run
12519    /// the whole priority ordering, converter pass and census for a
12520    /// connection whose socket is known to be full.
12521    ///
12522    /// The stream is registered even though the resume path reads its gid
12523    /// straight out of `expect_write` and needs no registration. That is what
12524    /// makes `vectored_calls` a witness: with the stream in the table, a pass
12525    /// that fell through into the scheduler phases would reach it again and
12526    /// issue a second `socket_write_vectored`.
12527    ///
12528    /// TO SEE THIS RED: delete the
12529    /// `return H2WriteTarget::Done(MuxResult::Continue);` from that arm,
12530    /// leaving the `if pass.stalled {}` with an empty body so control falls
12531    /// through to `self.stream_table.set_expect_write(None);`. Measured: this test fails
12532    /// on its own first assertion, `a stalled resume must leave expect_write
12533    /// parked`, with `left: None, right: Some(Other { id: 1, gid: 0 })`, and
12534    /// it is the ONLY failure in the suite. The
12535    /// second assertion moves under the same edit (`left: 2, right: 1`), the
12536    /// extra call being the main loop re-flushing the same stream through the
12537    /// delegated loopback socket once the one-entry script is spent.
12538    #[test]
12539    fn a_stalled_resume_does_not_begin_a_scheduler_pass() {
12540        let pool = make_pool_for_invariant_16();
12541        let (mut connection, mut context, gid, _peer) = writable_fixture(
12542            &pool,
12543            &[FIRST_BLOCK, SECOND_BLOCK],
12544            &[(0, SocketResult::WouldBlock)],
12545        );
12546        let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
12547
12548        let parked = H2StreamId::Other {
12549            id: REGISTERED_STREAM_ID,
12550            gid,
12551        };
12552        connection.core.stream_table.set_expect_write(Some(parked));
12553        assert_prepare_gate_is_shut(&context, gid);
12554
12555        let result = connection.writable(&mut context, EndpointClient(&mut router));
12556
12557        assert_eq!(
12558            connection.core.stream_table.expect_write(),
12559            Some(parked),
12560            "a stalled resume must leave expect_write parked: clearing it \
12561             would hand a socket-blocked stream back to the scheduler"
12562        );
12563        assert_eq!(
12564            connection.socket.vectored_calls, 1,
12565            "a stalled resume must return before the scheduler pass begins, so \
12566             the registered stream must not be flushed a second time"
12567        );
12568        assert!(
12569            !context.streams[gid].back.out.is_empty(),
12570            "the refused blocks must stay queued for the next pass"
12571        );
12572        assert_eq!(
12573            context.streams[gid].metrics.bout, 0,
12574            "a stalled resume delivered nothing"
12575        );
12576        assert!(
12577            matches!(result, MuxResult::Continue),
12578            "a stalled resume continues the session, got {result:?}"
12579        );
12580    }
12581
12582    /// A socket that takes at most `budget` bytes, then answers
12583    /// `WouldBlock`, and keeps every byte it took in `wire`, in order: what
12584    /// the peer would read. Reads are served from `inbound`.
12585    struct PacedSocket {
12586        stream: mio::net::TcpStream,
12587        budget: usize,
12588        wire: Vec<u8>,
12589        inbound: std::collections::VecDeque<u8>,
12590    }
12591
12592    impl PacedSocket {
12593        fn take(&mut self, buf: &[u8]) -> usize {
12594            let taken = self.budget.min(buf.len());
12595            self.wire.extend_from_slice(&buf[..taken]);
12596            self.budget -= taken;
12597            taken
12598        }
12599    }
12600
12601    impl SocketHandler for PacedSocket {
12602        fn socket_read(&mut self, buf: &mut [u8]) -> (usize, SocketResult) {
12603            let size = buf.len().min(self.inbound.len());
12604            for (slot, byte) in buf.iter_mut().zip(self.inbound.drain(..size)) {
12605                *slot = byte;
12606            }
12607            if size < buf.len() {
12608                (size, SocketResult::WouldBlock)
12609            } else {
12610                (size, SocketResult::Continue)
12611            }
12612        }
12613
12614        fn socket_write(&mut self, buf: &[u8]) -> (usize, SocketResult) {
12615            let taken = self.take(buf);
12616            if taken < buf.len() {
12617                (taken, SocketResult::WouldBlock)
12618            } else {
12619                (taken, SocketResult::Continue)
12620            }
12621        }
12622
12623        fn socket_write_vectored(&mut self, bufs: &[IoSlice]) -> (usize, SocketResult) {
12624            let mut total = 0;
12625            for buf in bufs {
12626                let taken = self.take(buf);
12627                total += taken;
12628                if taken < buf.len() {
12629                    return (total, SocketResult::WouldBlock);
12630                }
12631            }
12632            (total, SocketResult::Continue)
12633        }
12634
12635        fn socket_ref(&self) -> &mio::net::TcpStream {
12636            &self.stream
12637        }
12638
12639        fn socket_mut(&mut self) -> &mut mio::net::TcpStream {
12640            &mut self.stream
12641        }
12642
12643        fn peer_addr(&self) -> Option<std::net::SocketAddr> {
12644            None
12645        }
12646
12647        fn protocol(&self) -> crate::socket::TransportProtocol {
12648            crate::socket::TransportProtocol::Tcp
12649        }
12650
12651        fn read_error(&self) {}
12652
12653        fn write_error(&self) {}
12654    }
12655
12656    /// One frame as the peer reads it: `(type, flags, stream id, payload)`.
12657    type PeerFrame = (u8, u8, u32, Vec<u8>);
12658
12659    /// Split `wire` into frames the way the peer's parser does, failing on a
12660    /// frame the bytes cannot hold.
12661    fn peer_frames(mut wire: &[u8]) -> Result<Vec<PeerFrame>, String> {
12662        let mut frames = Vec::new();
12663        while !wire.is_empty() {
12664            if wire.len() < 9 {
12665                return Err(format!("{} trailing bytes: {wire:?}", wire.len()));
12666            }
12667            let len = u32::from_be_bytes([0, wire[0], wire[1], wire[2]]) as usize;
12668            let stream_id = u32::from_be_bytes([wire[5], wire[6], wire[7], wire[8]]) & 0x7fff_ffff;
12669            if wire.len() < 9 + len {
12670                return Err(format!(
12671                    "frame type {:#x} announces {len} bytes, {} remain",
12672                    wire[3],
12673                    wire.len() - 9
12674                ));
12675            }
12676            frames.push((wire[3], wire[4], stream_id, wire[9..9 + len].to_vec()));
12677            wire = &wire[9 + len..];
12678        }
12679        Ok(frames)
12680    }
12681
12682    /// The payload of the DATA frame `a_ping_during_a_half_written_frame_*`
12683    /// queues on stream 1, after its header below.
12684    const HALF_WRITTEN_PAYLOAD: &[u8] = &[b'd'; 32];
12685    const HALF_WRITTEN_HEADER: &[u8] = &[0, 0, 32, 0, 0, 0, 0, 0, 1];
12686
12687    /// Park a 41-byte DATA frame on stream 1 after the socket took 20 bytes
12688    /// of it, feed `inbound` (a PING or SETTINGS), and read it. `between`
12689    /// then runs before the socket is unthrottled, and the connection is
12690    /// driven the way `Mux` drives it, `writable()` and `readable()` when
12691    /// the filtered readiness allows them. Returns the connection, whose
12692    /// socket holds every byte the peer would read.
12693    fn ack_after_half_written_frame(
12694        inbound: &[u8],
12695        between: impl FnOnce(
12696            &mut H2Shell<PacedSocket>,
12697            &mut Context<TestListener>,
12698            &mut Router,
12699            GlobalStreamId,
12700        ),
12701    ) -> H2Shell<PacedSocket> {
12702        let (_pool, mut connection, mut context, mut router, gid) = park_half_written_frame();
12703
12704        connection.socket.inbound.extend(inbound);
12705        for _ in 0..8 {
12706            connection.core.readiness.event.insert(Ready::READABLE);
12707            if !connection.core.readiness.filter_interest().is_readable() {
12708                break;
12709            }
12710            connection.readable(&mut context, EndpointClient(&mut router));
12711        }
12712        assert!(
12713            connection.socket.inbound.is_empty() && !connection.core.output.is_empty(),
12714            "premise: the frame was read and its ACK queued"
12715        );
12716
12717        between(&mut connection, &mut context, &mut router, gid);
12718        drive_unthrottled(&mut connection, &mut context, &mut router);
12719        connection
12720    }
12721
12722    /// Drive `connection` the way `Mux` does, `writable()` and `readable()`
12723    /// when the filtered readiness allows them, with the socket unthrottled.
12724    fn drive_unthrottled(
12725        connection: &mut H2Shell<PacedSocket>,
12726        context: &mut Context<TestListener>,
12727        router: &mut Router,
12728    ) {
12729        connection.socket.budget = usize::MAX;
12730        for _ in 0..16 {
12731            connection.core.readiness.event.insert(Ready::WRITABLE);
12732            if connection.core.readiness.filter_interest().is_writable() {
12733                connection.writable(context, EndpointClient(router));
12734            }
12735            connection.core.readiness.event.insert(Ready::READABLE);
12736            if connection.core.readiness.filter_interest().is_readable() {
12737                connection.readable(context, EndpointClient(router));
12738            }
12739        }
12740    }
12741
12742    /// Park a 41-byte DATA frame on stream 1 after the socket took 20 bytes
12743    /// of it, and return the buffer pool, which must outlive the rest, the
12744    /// connection, its context and router, and the stream's global id.
12745    fn park_half_written_frame() -> (
12746        Rc<RefCell<Pool>>,
12747        H2Shell<PacedSocket>,
12748        Context<TestListener>,
12749        Router,
12750        GlobalStreamId,
12751    ) {
12752        park_cut_frames(&[HALF_WRITTEN_HEADER, HALF_WRITTEN_PAYLOAD], 20)
12753    }
12754
12755    /// [`park_half_written_frame`] for any whole frames of stream 1, queued
12756    /// as `blocks`, of which the socket takes `budget` bytes.
12757    fn park_cut_frames(
12758        blocks: &[&'static [u8]],
12759        budget: usize,
12760    ) -> (
12761        Rc<RefCell<Pool>>,
12762        H2Shell<PacedSocket>,
12763        Context<TestListener>,
12764        Router,
12765        GlobalStreamId,
12766    ) {
12767        let pool = make_pool_for_invariant_16();
12768        let (socket, _peer) = connected_socket();
12769        let mut connection = H2Shell::new(
12770            Ulid::generate(),
12771            PacedSocket {
12772                stream: socket,
12773                budget,
12774                wire: Vec::new(),
12775                inbound: std::collections::VecDeque::new(),
12776            },
12777            Position::Server,
12778            &mut PoolBufferSource::new(Rc::downgrade(&pool)),
12779            H2FloodConfig::default(),
12780            H2ConnectionConfig::default(),
12781            Duration::from_secs(30),
12782            None,
12783            Duration::from_secs(30),
12784            Some((H2StreamId::Zero, 9)),
12785            Ready::READABLE | Ready::WRITABLE | Ready::HUP | Ready::ERROR,
12786        )
12787        .expect("a pool with free buffers must yield an H2 connection");
12788        connection.core.state = H2State::Header;
12789        let mut context = test_context(&pool);
12790        let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
12791        let gid = context
12792            .create_stream(Ulid::generate(), 1 << 16)
12793            .expect("test context must create a stream");
12794        connection
12795            .core
12796            .stream_table
12797            .register(REGISTERED_STREAM_ID, gid, connection.core.now);
12798        for &block in blocks {
12799            context.streams[gid]
12800                .back
12801                .out
12802                .push_back(kawa::OutBlock::Store(kawa::Store::Static(block)));
12803        }
12804        assert_prepare_gate_is_shut(&context, gid);
12805
12806        connection.core.readiness.event.insert(Ready::WRITABLE);
12807        connection.writable(&mut context, EndpointClient(&mut router));
12808        assert_eq!(
12809            (
12810                connection.socket.wire.len(),
12811                connection.core.stream_table.expect_write()
12812            ),
12813            (
12814                budget,
12815                Some(H2StreamId::Other {
12816                    id: REGISTERED_STREAM_ID,
12817                    gid
12818                })
12819            ),
12820            "premise: the frames are cut and their stream parked"
12821        );
12822        (pool, connection, context, router, gid)
12823    }
12824
12825    const HALF_WRITTEN_PING: &[u8] = b"\x00\x00\x08\x06\x00\x00\x00\x00\x00pingpong";
12826    const SECOND_PING: &[u8] = b"\x00\x00\x08\x06\x00\x00\x00\x00\x00pingpon2";
12827
12828    /// `(type, flags, stream id)` of every frame the peer reads, or the
12829    /// parse failure.
12830    fn peer_frame_kinds(wire: &[u8]) -> Result<Vec<(u8, u8, u32)>, String> {
12831        peer_frames(wire).map(|frames| {
12832            frames
12833                .into_iter()
12834                .map(|(kind, flags, stream_id, _)| (kind, flags, stream_id))
12835                .collect()
12836        })
12837    }
12838
12839    /// A PING that arrives while a stream frame is half-written must be
12840    /// acknowledged AFTER that frame, never inside it (#1600).
12841    ///
12842    /// The socket takes 20 bytes of a 41-byte DATA frame, so the stream is
12843    /// parked in `expect_write` and the 21-byte rest of its frame is adopted
12844    /// by the ordered output queue (#1604). The peer then sends a PING. Its
12845    /// ACK is queued behind that rest, so the peer reads the whole DATA
12846    /// frame, then the ACK, and never 17 ACK bytes in the middle of the
12847    /// frame.
12848    ///
12849    /// TO SEE THIS RED: in `ConnectionH2::handle_write`, replace
12850    /// `self.output.adopt_tail(kawa, tail);` by `let _ = (kawa, tail);`, so
12851    /// the rest of the cut frame stays with its stream. Every test on this
12852    /// fixture then fails: this one and the other #1601 tests on
12853    /// `frame_tail`'s `a frame runs past the gathered bytes` debug assertion
12854    /// (the stream's `kawa.out` no longer starts on a frame boundary), the
12855    /// three #1604 tests below on their wire assertion. Verified 2026-09-27.
12856    /// Before #1604 the equivalent mutation was in `queue_zero_output` and
12857    /// failed with `left: Err("frame type 0x64 announces 6579300 bytes, 8
12858    /// remain")` (verified 2026-09-26).
12859    #[test]
12860    fn a_ping_during_a_half_written_frame_is_acknowledged_after_it() {
12861        let connection = ack_after_half_written_frame(HALF_WRITTEN_PING, |_, _, _, _| {});
12862
12863        assert_eq!(
12864            peer_frames(&connection.socket.wire),
12865            Ok(vec![
12866                (0, 0, REGISTERED_STREAM_ID, HALF_WRITTEN_PAYLOAD.to_vec()),
12867                (6, parser::FLAG_ACK, 0, b"pingpong".to_vec()),
12868            ]),
12869            "the peer must read the whole DATA frame, then the PING ACK"
12870        );
12871        assert!(
12872            connection.core.readiness.interest.is_readable(),
12873            "once the ACK is out the connection must read again"
12874        );
12875    }
12876
12877    /// The SETTINGS ACK takes the same path as the PING ACK above (#1600).
12878    #[test]
12879    fn a_settings_during_a_half_written_frame_is_acknowledged_after_it() {
12880        let connection =
12881            ack_after_half_written_frame(b"\x00\x00\x00\x04\x00\x00\x00\x00\x00", |_, _, _, _| {});
12882
12883        assert_eq!(
12884            peer_frames(&connection.socket.wire),
12885            Ok(vec![
12886                (0, 0, REGISTERED_STREAM_ID, HALF_WRITTEN_PAYLOAD.to_vec()),
12887                (4, parser::FLAG_ACK, 0, Vec::new()),
12888            ]),
12889            "the peer must read the whole DATA frame, then the SETTINGS ACK"
12890        );
12891        assert!(
12892            connection.core.readiness.interest.is_readable(),
12893            "once the ACK is out the connection must read again"
12894        );
12895    }
12896
12897    /// An ACK queued behind a half-written frame whose stream is removed
12898    /// before the frame resumes still leaves, and the connection keeps
12899    /// reading: the removal takes nothing that was queued. What the peer
12900    /// reads is asserted by
12901    /// `a_removed_half_written_stream_still_completes_its_frame` (#1604).
12902    ///
12903    /// TO SEE THIS RED: in `ConnectionH2::flush_pending_control_frames`,
12904    /// answer `H2ControlFlushTarget::Proceed` unconditionally at its end. The
12905    /// test then fails with `an ACK behind a removed stream's frame must not
12906    /// be stranded`.
12907    #[test]
12908    fn a_deferred_ack_does_not_strand_reads_when_its_parked_stream_is_removed() {
12909        let connection =
12910            ack_after_half_written_frame(HALF_WRITTEN_PING, |connection, _, _, gid| {
12911                connection
12912                    .core
12913                    .remove_dead_stream(REGISTERED_STREAM_ID, gid);
12914            });
12915
12916        assert!(
12917            connection.core.output.is_empty(),
12918            "an ACK behind a removed stream's frame must not be stranded"
12919        );
12920        assert!(
12921            connection.core.readiness.interest.is_readable(),
12922            "once the output is flushed the connection must read again"
12923        );
12924    }
12925
12926    /// Two PINGs read while one frame stays parked are both acknowledged,
12927    /// in order, after that frame.
12928    #[test]
12929    fn two_pings_during_one_park_are_acknowledged_after_the_frame() {
12930        let connection = ack_after_half_written_frame(HALF_WRITTEN_PING, |connection, _, _, _| {
12931            connection.socket.inbound.extend(SECOND_PING);
12932        });
12933
12934        assert_eq!(
12935            peer_frames(&connection.socket.wire),
12936            Ok(vec![
12937                (0, 0, REGISTERED_STREAM_ID, HALF_WRITTEN_PAYLOAD.to_vec()),
12938                (6, parser::FLAG_ACK, 0, b"pingpong".to_vec()),
12939                (6, parser::FLAG_ACK, 0, b"pingpon2".to_vec()),
12940            ]),
12941            "both PING ACKs must follow the whole DATA frame, in order"
12942        );
12943    }
12944
12945    /// A SETTINGS then a PING during one park: both ACKs, in order.
12946    #[test]
12947    fn settings_then_ping_during_one_park_are_acknowledged_after_the_frame() {
12948        let connection = ack_after_half_written_frame(
12949            b"\x00\x00\x00\x04\x00\x00\x00\x00\x00",
12950            |connection, _, _, _| {
12951                connection.socket.inbound.extend(SECOND_PING);
12952            },
12953        );
12954
12955        assert_eq!(
12956            peer_frames(&connection.socket.wire),
12957            Ok(vec![
12958                (0, 0, REGISTERED_STREAM_ID, HALF_WRITTEN_PAYLOAD.to_vec()),
12959                (4, parser::FLAG_ACK, 0, Vec::new()),
12960                (6, parser::FLAG_ACK, 0, b"pingpon2".to_vec()),
12961            ]),
12962            "the SETTINGS ACK then the PING ACK must follow the whole DATA frame"
12963        );
12964    }
12965
12966    /// The shutdown drive (`Mux::shutting_down`) calls `flush_output_buffer`,
12967    /// then forces READABLE and calls `readable()`. That read must not eat
12968    /// the queued ACK. Since #1604 it cannot by construction: the read lands
12969    /// in `zero`, which is input only, and the ACK waits in the output queue
12970    /// behind the rest of the half-written frame.
12971    ///
12972    /// TO SEE THIS RED: the `adopt_tail` mutation described on
12973    /// `a_ping_during_a_half_written_frame_is_acknowledged_after_it` fails it
12974    /// on `a frame runs past the gathered bytes`. Verified 2026-09-27. Before
12975    /// #1604, deleting the `zero_holds_output()` block of `poll_read_target`
12976    /// failed it on `no output may be pending in zero when a frame header is
12977    /// read` (verified 2026-09-27).
12978    #[test]
12979    fn a_forced_shutdown_read_does_not_eat_a_deferred_ack() {
12980        let connection =
12981            ack_after_half_written_frame(HALF_WRITTEN_PING, |connection, context, router, _| {
12982                connection.socket.inbound.extend(SECOND_PING);
12983                connection.flush_output_buffer();
12984                connection.core.readiness.interest.insert(Ready::READABLE);
12985                connection.core.readiness.event.insert(Ready::READABLE);
12986                connection.readable(context, EndpointClient(router));
12987            });
12988
12989        assert_eq!(
12990            peer_frames(&connection.socket.wire),
12991            Ok(vec![
12992                (0, 0, REGISTERED_STREAM_ID, HALF_WRITTEN_PAYLOAD.to_vec()),
12993                (6, parser::FLAG_ACK, 0, b"pingpong".to_vec()),
12994                (6, parser::FLAG_ACK, 0, b"pingpon2".to_vec()),
12995            ]),
12996            "the forced shutdown read must not eat the deferred ACK"
12997        );
12998    }
12999
13000    /// A final GOAWAY while a frame is parked and an ACK queued is sent after
13001    /// both: `goaway()` appends to the ordered output queue, behind the rest
13002    /// of the frame and the ACK.
13003    ///
13004    /// TO SEE THIS RED: in `ConnectionH2::goaway`, call `self.output.clear()`
13005    /// before queueing the GOAWAY. The test then fails with `the whole DATA
13006    /// frame, the PING ACK, then the GOAWAY`. Verified 2026-09-27.
13007    #[test]
13008    fn a_goaway_during_a_half_written_frame_follows_it_and_the_ack() {
13009        let connection =
13010            ack_after_half_written_frame(HALF_WRITTEN_PING, |connection, context, _, gid| {
13011                // A stream being answered is linked; an idle one would be
13012                // pruned by the closing connection before its frame resumes.
13013                context.streams[gid].state = StreamState::Linked(mio::Token(1));
13014                let _ = connection.core.goaway(H2Error::NoError);
13015            });
13016
13017        assert_eq!(
13018            peer_frame_kinds(&connection.socket.wire),
13019            Ok(vec![
13020                (0, 0, REGISTERED_STREAM_ID),
13021                (6, parser::FLAG_ACK, 0),
13022                (7, 0, 0)
13023            ]),
13024            "the whole DATA frame, the PING ACK, then the GOAWAY"
13025        );
13026    }
13027
13028    /// The initial graceful GOAWAY (`graceful_goaway` -> `send_initial_goaway`,
13029    /// the call `Mux::shutting_down` makes) behaves like the final one.
13030    ///
13031    /// TO SEE THIS RED: in `ConnectionH2::send_initial_goaway`, call
13032    /// `self.output.clear()` before queueing the GOAWAY. The test then fails
13033    /// with `the whole DATA frame, the PING ACK, then the initial GOAWAY`.
13034    /// Verified 2026-09-27.
13035    #[test]
13036    fn an_initial_goaway_during_a_half_written_frame_follows_it_and_the_ack() {
13037        let connection = ack_after_half_written_frame(HALF_WRITTEN_PING, |connection, _, _, _| {
13038            let now = connection.core.now;
13039            let _ = connection.core.graceful_goaway(now);
13040        });
13041
13042        assert_eq!(
13043            peer_frame_kinds(&connection.socket.wire),
13044            Ok(vec![
13045                (0, 0, REGISTERED_STREAM_ID),
13046                (6, parser::FLAG_ACK, 0),
13047                (7, 0, 0)
13048            ]),
13049            "the whole DATA frame, the PING ACK, then the initial GOAWAY"
13050        );
13051    }
13052
13053    /// A peer RST_STREAM for stream 1, whose DATA frame the fixture parks.
13054    const RST_STREAM_1: &[u8] = b"\x00\x00\x04\x03\x00\x00\x00\x00\x01\x00\x00\x00\x08";
13055
13056    /// A stream parked half-way through a frame and then removed must not
13057    /// leave that frame truncated on the wire (#1604): the bytes written
13058    /// after the removal would land inside what the peer still parses as
13059    /// that frame. Here the removal is `remove_dead_stream` itself, the
13060    /// chokepoint of every removal path, and the next frame is a PING ACK
13061    /// read after the removal.
13062    ///
13063    /// TO SEE THIS RED: on the pre-image, `a0785505` (stream frames written
13064    /// from `kawa.out` and serialised only through `expect_write`), this test
13065    /// and its two siblings below fail on their wire assertion with, in
13066    /// order, `left: Err("frame type 0x0 announces 65536 bytes, 4 remain")`,
13067    /// `... 0 remain")` and `left: Err("frame type 0x0 announces 32 bytes,
13068    /// 28 remain")`: the peer reads the next frame's bytes as the rest of the
13069    /// truncated one. On this tree, the `adopt_tail` mutation described on
13070    /// `a_ping_during_a_half_written_frame_is_acknowledged_after_it` fails
13071    /// all three with the same messages. Verified 2026-09-27.
13072    #[test]
13073    fn a_removed_half_written_stream_still_completes_its_frame() {
13074        let connection =
13075            ack_after_half_written_frame(HALF_WRITTEN_PING, |connection, _, _, gid| {
13076                connection
13077                    .core
13078                    .remove_dead_stream(REGISTERED_STREAM_ID, gid);
13079                connection.socket.inbound.extend(SECOND_PING);
13080            });
13081
13082        assert_eq!(
13083            peer_frames(&connection.socket.wire),
13084            Ok(vec![
13085                (0, 0, REGISTERED_STREAM_ID, HALF_WRITTEN_PAYLOAD.to_vec()),
13086                (6, parser::FLAG_ACK, 0, b"pingpong".to_vec()),
13087                (6, parser::FLAG_ACK, 0, b"pingpon2".to_vec()),
13088            ]),
13089            "the removed stream's frame must be completed before the ACKs"
13090        );
13091        assert!(
13092            connection.core.readiness.interest.is_readable(),
13093            "once the output is flushed the connection must read again"
13094        );
13095    }
13096
13097    /// The same removal followed by another stream's frame (#1604): the
13098    /// next stream's DATA frame must follow the whole parked frame.
13099    #[test]
13100    fn a_removed_half_written_stream_does_not_corrupt_the_next_stream_frame() {
13101        const NEXT_STREAM_ID: StreamId = 3;
13102        const NEXT_HEADER: &[u8] = &[0, 0, 4, 0, 1, 0, 0, 0, 3];
13103        const NEXT_PAYLOAD: &[u8] = b"next";
13104        let connection =
13105            ack_after_half_written_frame(HALF_WRITTEN_PING, |connection, context, _, gid| {
13106                connection
13107                    .core
13108                    .remove_dead_stream(REGISTERED_STREAM_ID, gid);
13109                let next = context
13110                    .create_stream(Ulid::generate(), 1 << 16)
13111                    .expect("test context must create a second stream");
13112                connection
13113                    .core
13114                    .stream_table
13115                    .register(NEXT_STREAM_ID, next, connection.core.now);
13116                for block in [NEXT_HEADER, NEXT_PAYLOAD] {
13117                    context.streams[next]
13118                        .back
13119                        .out
13120                        .push_back(kawa::OutBlock::Store(kawa::Store::Static(block)));
13121                }
13122            });
13123
13124        assert_eq!(
13125            peer_frames(&connection.socket.wire),
13126            Ok(vec![
13127                (0, 0, REGISTERED_STREAM_ID, HALF_WRITTEN_PAYLOAD.to_vec()),
13128                (6, parser::FLAG_ACK, 0, b"pingpong".to_vec()),
13129                (0, 1, NEXT_STREAM_ID, NEXT_PAYLOAD.to_vec()),
13130            ]),
13131            "the next stream's frame must follow the whole parked frame"
13132        );
13133    }
13134
13135    /// HEADERS without END_HEADERS, then its CONTINUATION, on stream 1.
13136    const CUT_BLOCK_HEADERS: &[u8] = b"\x00\x00\x10\x01\x00\x00\x00\x00\x01hpack-fragment-1";
13137    const CUT_BLOCK_CONTINUATION: &[u8] = b"\x00\x00\x10\x09\x04\x00\x00\x00\x01hpack-fragment-2";
13138
13139    /// A header block cut by a partial write keeps everything else out until
13140    /// END_HEADERS (RFC 9113 §6.10): the PING ACK read meanwhile follows the
13141    /// CONTINUATION, whether the write stopped inside the HEADERS frame or
13142    /// exactly on the boundary between the two frames, where no frame is
13143    /// half-written but the block still is (#1604).
13144    ///
13145    /// TO SEE THIS RED: in `h2_transmit::frame_tail`, append `&& false` to
13146    /// the `in_block` expression, so the rest stops at the cut frame's end.
13147    /// The test then fails with `the ACK must follow END_HEADERS`, the ACK
13148    /// sitting between the HEADERS and the CONTINUATION. Verified
13149    /// 2026-09-27.
13150    #[test]
13151    fn a_cut_header_block_is_completed_before_the_ack() {
13152        for budget in [20, CUT_BLOCK_HEADERS.len()] {
13153            let (_pool, mut connection, mut context, mut router, _gid) =
13154                park_cut_frames(&[CUT_BLOCK_HEADERS, CUT_BLOCK_CONTINUATION], budget);
13155            connection.socket.inbound.extend(HALF_WRITTEN_PING);
13156            for _ in 0..4 {
13157                connection.core.readiness.event.insert(Ready::READABLE);
13158                connection.readable(&mut context, EndpointClient(&mut router));
13159            }
13160            assert!(
13161                connection.socket.inbound.is_empty(),
13162                "premise: the PING was read while the block is cut"
13163            );
13164            drive_unthrottled(&mut connection, &mut context, &mut router);
13165
13166            assert_eq!(
13167                peer_frame_kinds(&connection.socket.wire),
13168                Ok(vec![
13169                    (1, 0, REGISTERED_STREAM_ID),
13170                    (9, parser::FLAG_END_HEADERS, REGISTERED_STREAM_ID),
13171                    (6, parser::FLAG_ACK, 0),
13172                ]),
13173                "the ACK must follow END_HEADERS (cut at {budget})"
13174            );
13175        }
13176    }
13177
13178    /// The peer resets the parked stream (#1604). Its RST_STREAM is read
13179    /// while the frame is half-written, and retires the stream; the rest of
13180    /// that frame must still reach the wire before the PING ACK behind it.
13181    #[test]
13182    fn a_peer_reset_of_a_half_written_stream_still_completes_its_frame() {
13183        let (_pool, mut connection, mut context, mut router, _gid) = park_half_written_frame();
13184        connection.socket.inbound.extend(RST_STREAM_1);
13185        connection.core.readiness.event.insert(Ready::READABLE);
13186        connection.readable(&mut context, EndpointClient(&mut router));
13187        connection.core.readiness.event.insert(Ready::READABLE);
13188        connection.readable(&mut context, EndpointClient(&mut router));
13189        assert!(
13190            connection.socket.inbound.is_empty()
13191                && !connection
13192                    .core
13193                    .stream_table
13194                    .streams()
13195                    .contains_key(&REGISTERED_STREAM_ID),
13196            "premise: the RST was read and retired the parked stream"
13197        );
13198
13199        connection.socket.inbound.extend(HALF_WRITTEN_PING);
13200        drive_unthrottled(&mut connection, &mut context, &mut router);
13201
13202        assert_eq!(
13203            peer_frames(&connection.socket.wire),
13204            Ok(vec![
13205                (0, 0, REGISTERED_STREAM_ID, HALF_WRITTEN_PAYLOAD.to_vec()),
13206                (6, parser::FLAG_ACK, 0, b"pingpong".to_vec()),
13207            ]),
13208            "the reset stream's frame must be completed before the ACK"
13209        );
13210    }
13211
13212    /// Park stream 1's encoded 404 unsent behind a socket that takes nothing,
13213    /// let `drop_block` discard it, then drive the connection unthrottled and
13214    /// decode every HEADERS the peer reads with ONE decoder, as the peer does.
13215    /// Answers `(stream id, decoded)` per HEADERS and the `:status` values read.
13216    ///
13217    /// Both requests get the same 404, so stream 1's block inserts the
13218    /// answer's fields into the connection's encoder table and stream 3's
13219    /// identical block names them by index: a peer that missed stream 1's
13220    /// block cannot read stream 3's without a table reset.
13221    fn peer_reads_after_a_dropped_parked_block(
13222        drop_block: impl FnOnce(&mut H2Shell<PacedSocket>, &mut Context<TestListener>, &mut Router),
13223    ) -> (Vec<(u32, bool)>, Vec<Vec<u8>>) {
13224        let (_pool, mut connection, mut context, mut router, _peer) = two_requests_read(0);
13225        let answers = context.listener.borrow().get_answers().clone();
13226        let open: Vec<GlobalStreamId> = connection
13227            .core
13228            .stream_table
13229            .streams()
13230            .values()
13231            .copied()
13232            .collect();
13233        for gid in open {
13234            crate::protocol::mux::answers::set_default_answer(
13235                &mut context.streams[gid],
13236                &mut connection.core.readiness,
13237                404,
13238                &answers.borrow(),
13239            );
13240        }
13241        connection.core.readiness.event.insert(Ready::WRITABLE);
13242        connection.writable(&mut context, EndpointClient(&mut router));
13243        assert!(
13244            connection.socket.wire.is_empty()
13245                && connection.core.stream_table.expect_write().is_some(),
13246            "premise: stream 1's block was encoded and parked unsent"
13247        );
13248        drop_block(&mut connection, &mut context, &mut router);
13249        connection.socket.budget = usize::MAX;
13250        drive_both_ways(&mut connection, &mut context, &mut router);
13251        peer_header_blocks(&connection.socket.wire)
13252    }
13253
13254    /// An H2 server connection over a [`PacedSocket`] taking `budget` bytes,
13255    /// with GET requests on streams 1 and 3 read and not answered.
13256    fn two_requests_read(
13257        budget: usize,
13258    ) -> (
13259        Rc<RefCell<Pool>>,
13260        H2Shell<PacedSocket>,
13261        Context<TestListener>,
13262        Router,
13263        std::net::TcpStream,
13264    ) {
13265        let pool = make_pool_for_invariant_16();
13266        let (socket, peer) = connected_socket();
13267        let mut connection = H2Shell::new(
13268            Ulid::generate(),
13269            PacedSocket {
13270                stream: socket,
13271                budget,
13272                wire: Vec::new(),
13273                inbound: std::collections::VecDeque::new(),
13274            },
13275            Position::Server,
13276            &mut PoolBufferSource::new(Rc::downgrade(&pool)),
13277            H2FloodConfig::default(),
13278            H2ConnectionConfig::default(),
13279            Duration::from_secs(30),
13280            None,
13281            Duration::from_secs(30),
13282            Some((H2StreamId::Zero, 9)),
13283            Ready::READABLE | Ready::WRITABLE | Ready::HUP | Ready::ERROR,
13284        )
13285        .expect("pool");
13286        connection.core.state = H2State::Header;
13287        let mut context = test_context(&pool);
13288        let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
13289        let mut peer_encoder = crate::protocol::mux::hpack::Encoder::new();
13290        for stream_id in [1u32, 3] {
13291            let block = peer_encoder.encode([
13292                (&b":method"[..], &b"GET"[..]),
13293                (&b":scheme"[..], &b"https"[..]),
13294                (&b":authority"[..], &b"example.com"[..]),
13295                (&b":path"[..], &b"/repro"[..]),
13296            ]);
13297            let mut frame = (block.len() as u32).to_be_bytes()[1..].to_vec();
13298            frame.push(1);
13299            frame.push(parser::FLAG_END_STREAM | parser::FLAG_END_HEADERS);
13300            frame.extend_from_slice(&stream_id.to_be_bytes());
13301            frame.extend_from_slice(&block);
13302            connection.socket.inbound.extend(frame);
13303        }
13304        for _ in 0..8 {
13305            connection.core.readiness.event.insert(Ready::READABLE);
13306            connection.readable(&mut context, EndpointClient(&mut router));
13307        }
13308        assert_eq!(
13309            connection.core.stream_table.len(),
13310            2,
13311            "premise: both requests were read"
13312        );
13313        (pool, connection, context, router, peer)
13314    }
13315
13316    /// Drive the connection the way `Mux` does, `writable()` and
13317    /// `readable()` when the filtered readiness allows them.
13318    fn drive_both_ways(
13319        connection: &mut H2Shell<PacedSocket>,
13320        context: &mut Context<TestListener>,
13321        router: &mut Router,
13322    ) {
13323        for _ in 0..16 {
13324            connection.core.readiness.event.insert(Ready::WRITABLE);
13325            if connection.core.readiness.filter_interest().is_writable() {
13326                connection.writable(context, EndpointClient(router));
13327            }
13328            connection.core.readiness.event.insert(Ready::READABLE);
13329            if connection.core.readiness.filter_interest().is_readable() {
13330                connection.readable(context, EndpointClient(router));
13331            }
13332        }
13333    }
13334
13335    /// Decode every HEADERS on `wire` with ONE decoder, as the peer does.
13336    /// Answers `(stream id, decoded)` per HEADERS and the `:status` values read.
13337    fn peer_header_blocks(wire: &[u8]) -> (Vec<(u32, bool)>, Vec<Vec<u8>>) {
13338        let mut decoder = crate::protocol::mux::hpack::Decoder::new();
13339        let mut seen = Vec::new();
13340        let mut status = Vec::new();
13341        for (kind, _, stream_id, payload) in peer_frames(wire).unwrap() {
13342            if kind == 1 {
13343                let decoded = decoder.decode_with_cb(&payload, |name, value| {
13344                    if &*name == b":status" {
13345                        status.push(value.into_owned());
13346                    }
13347                });
13348                seen.push((stream_id, decoded.is_ok()));
13349            }
13350        }
13351        (seen, status)
13352    }
13353
13354    /// A header block the encoder produced but the wire never started is
13355    /// dropped with its stream, and the peer's HPACK table must not fall
13356    /// behind for it (sozu-proxy/sozu#1627, path 1): the peer's RST_STREAM(1)
13357    /// removes the parked stream 1, and stream 3's block must still decode.
13358    ///
13359    /// TO SEE THIS RED: in `ConnectionH2::poll_write_target`'s
13360    /// `H2WritePhase::Start` arm, delete the `self.reset_encoder_table()` call.
13361    /// The peer then fails to decode stream 3's block: `left: [(3, false)],
13362    /// right: [(3, true)]`. Verified 2026-09-27 (red on `58550dd4`).
13363    #[test]
13364    fn a_header_block_dropped_with_its_stream_keeps_the_peer_table_in_sync() {
13365        let (seen, status) =
13366            peer_reads_after_a_dropped_parked_block(|connection, context, router| {
13367                connection.socket.inbound.extend(RST_STREAM_1);
13368                for _ in 0..4 {
13369                    connection.core.readiness.event.insert(Ready::READABLE);
13370                    connection.readable(context, EndpointClient(router));
13371                }
13372                assert!(
13373                    !connection.core.stream_table.streams().contains_key(&1),
13374                    "premise: the RST removed stream 1"
13375                );
13376            });
13377        assert_eq!(
13378            seen,
13379            vec![(3, true)],
13380            "the peer must decode stream 3's block with its own table"
13381        );
13382        assert_eq!(status, vec![b"404".to_vec()], "and read what was encoded");
13383    }
13384
13385    /// The park outlives a second write pass that is stalled too: that pass
13386    /// resumes the parked stream, writes nothing, and parks it again, so it
13387    /// must record again that the park holds an encoded block. The peer's
13388    /// RST_STREAM(1) then removes it, and stream 3's block must still decode
13389    /// (sozu-proxy/sozu#1627).
13390    ///
13391    /// TO SEE THIS RED: in `ConnectionH2::poll_write_target`'s
13392    /// `H2WritePhase::Resume` arm, delete the `self.parked_header_block = ..`
13393    /// assignment before the stalled `return`. The peer then fails on stream
13394    /// 3's block: `left: [(3, false)], right: [(3, true)]`. Verified
13395    /// 2026-09-27.
13396    #[test]
13397    fn a_header_block_parked_across_two_stalled_passes_keeps_the_peer_table_in_sync() {
13398        let (seen, status) =
13399            peer_reads_after_a_dropped_parked_block(|connection, context, router| {
13400                connection.core.readiness.event.insert(Ready::WRITABLE);
13401                connection.writable(context, EndpointClient(router));
13402                assert!(
13403                    connection.socket.wire.is_empty()
13404                        && connection.core.stream_table.expect_write().is_some(),
13405                    "premise: the second pass resumed nothing and kept the park"
13406                );
13407                connection.socket.inbound.extend(RST_STREAM_1);
13408                for _ in 0..4 {
13409                    connection.core.readiness.event.insert(Ready::READABLE);
13410                    connection.readable(context, EndpointClient(router));
13411                }
13412                assert!(
13413                    !connection.core.stream_table.streams().contains_key(&1),
13414                    "premise: the RST removed stream 1"
13415                );
13416            });
13417        assert_eq!(
13418            seen,
13419            vec![(3, true)],
13420            "the peer must decode stream 3's block with its own table"
13421        );
13422        assert_eq!(status, vec![b"404".to_vec()], "and read what was encoded");
13423    }
13424
13425    /// An H2 backend connection (`Position::Client`, `Connected`, handshake
13426    /// done) over a [`PacedSocket`] taking `budget` bytes.
13427    fn paced_backend(pool: &Rc<RefCell<Pool>>, budget: usize) -> H2Shell<PacedSocket> {
13428        let (socket, _peer) = connected_socket();
13429        let backend = Rc::new(RefCell::new(crate::backends::Backend::new(
13430            "relink-backend",
13431            "127.0.0.1:2".parse().expect("backend address must parse"),
13432            None,
13433            None,
13434            None,
13435        )));
13436        let mut registry = crate::protocol::mux::BackendRegistry::default();
13437        let mut connection = H2Shell::new(
13438            Ulid::generate(),
13439            PacedSocket {
13440                stream: socket,
13441                budget,
13442                wire: Vec::new(),
13443                inbound: std::collections::VecDeque::new(),
13444            },
13445            Position::Client(
13446                "relink-cluster".into(),
13447                registry.id_for(&backend),
13448                BackendStatus::Connected,
13449            ),
13450            &mut PoolBufferSource::new(Rc::downgrade(pool)),
13451            H2FloodConfig::default(),
13452            H2ConnectionConfig::default(),
13453            Duration::from_secs(30),
13454            None,
13455            Duration::from_secs(30),
13456            Some((H2StreamId::Zero, 9)),
13457            Ready::READABLE | Ready::WRITABLE | Ready::HUP | Ready::ERROR,
13458        )
13459        .expect("a pool with free buffers must yield an H2 connection");
13460        connection.core.state = H2State::Header;
13461        connection
13462    }
13463
13464    /// Queue a GET request carrying `x-api-key: <key>` on `gid`'s request
13465    /// side, closed with END_STREAM, the way a relayed request reaches a
13466    /// backend's write pass. The custom field is indexed into the encoder's
13467    /// dynamic table, as a credential-like header would be.
13468    fn queue_backend_request(
13469        context: &mut Context<TestListener>,
13470        gid: GlobalStreamId,
13471        key: &'static [u8],
13472    ) {
13473        let kawa = &mut context.streams[gid].front;
13474        kawa.detached.status_line = kawa::StatusLine::Request {
13475            version: kawa::Version::V20,
13476            method: kawa::Store::Static(b"GET"),
13477            uri: kawa::Store::Static(b"/"),
13478            authority: kawa::Store::Static(b"example.com"),
13479            path: kawa::Store::Static(b"/"),
13480        };
13481        kawa.push_block(kawa::Block::StatusLine);
13482        kawa.push_block(kawa::Block::Header(kawa::Pair {
13483            key: kawa::Store::Static(b"x-api-key"),
13484            val: kawa::Store::Static(key),
13485        }));
13486        kawa.push_block(kawa::Block::Flags(kawa::Flags {
13487            end_body: false,
13488            end_chunk: false,
13489            end_header: true,
13490            end_stream: true,
13491        }));
13492        kawa.parsing_phase = kawa::ParsingPhase::Terminated;
13493    }
13494
13495    /// The #1632 starting point: a request linked to backend `backend`,
13496    /// whose write pass HPACK-encoded its HEADERS while the socket took
13497    /// nothing. Answers the request's global id and the exact bytes that
13498    /// connection encoded for it.
13499    fn park_an_encoded_request(
13500        backend: &mut H2Shell<PacedSocket>,
13501        context: &mut Context<TestListener>,
13502        router: &mut Router,
13503    ) -> (GlobalStreamId, Vec<u8>) {
13504        let gid = context
13505            .create_stream(Ulid::generate(), 1 << 16)
13506            .expect("test context must create a stream");
13507        queue_backend_request(context, gid, b"alice-secret");
13508        context.streams[gid].state = StreamState::Linked(mio::Token(1));
13509        assert!(backend.start_stream(gid, context), "premise: stream 1");
13510        backend.core.readiness.event.insert(Ready::WRITABLE);
13511        backend.writable(context, EndpointClient(router));
13512        let front = &context.streams[gid].front;
13513        assert!(
13514            backend.socket.wire.is_empty() && !front.out.is_empty() && !front.consumed,
13515            "premise: the request was encoded and parked unsent"
13516        );
13517        let encoded: Vec<u8> = front
13518            .as_io_slice()
13519            .iter()
13520            .flat_map(|slice| slice.iter().copied())
13521            .collect();
13522        (gid, encoded)
13523    }
13524
13525    /// RFC 9113 §6.9: a proxied stream has an independent send window on each
13526    /// connection. The client granted the maximum initial window on the
13527    /// frontend connection; the H2 backend then advertises
13528    /// `SETTINGS_INITIAL_WINDOW_SIZE = 2^31-1` too. Each value is within the
13529    /// §6.9.2 ceiling, so the backend connection must acknowledge it and send
13530    /// the request, not overflow a window it shares with the frontend leg and
13531    /// answer GOAWAY.
13532    ///
13533    /// TO SEE THIS RED: make `Stream::send_window_mut` and `Stream::split`
13534    /// answer `front_window` for `Position::Client` too. The backend then
13535    /// adds its delta to the client's 2^31-1: `the backend connection must
13536    /// not answer a valid initial window with GOAWAY`. Verified 2026-10-01
13537    /// (red on `3d3f077b`).
13538    #[test]
13539    fn a_backend_max_initial_window_is_independent_of_the_frontend_window() {
13540        let pool = make_pool_for_invariant_16();
13541        let mut backend = paced_backend(&pool, usize::MAX);
13542        let mut context = test_context(&pool);
13543        let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
13544        // What a frontend connection whose client advertised 2^31-1 creates.
13545        let gid = context
13546            .create_stream(Ulid::generate(), FLOW_CONTROL_MAX_WINDOW)
13547            .expect("test context must create a stream");
13548        queue_backend_request(&mut context, gid, b"key");
13549        context.streams[gid].state = StreamState::Linked(mio::Token(1));
13550        assert!(backend.start_stream(gid, &mut context), "premise: stream 1");
13551
13552        let mut max_initial_window = vec![0, 4];
13553        max_initial_window.extend_from_slice(&FLOW_CONTROL_MAX_WINDOW.to_be_bytes());
13554        backend
13555            .socket
13556            .inbound
13557            .extend(orphan_frame(4, 0, 0, 6, &max_initial_window));
13558        for _ in 0..4 {
13559            backend.core.readiness.event.insert(Ready::READABLE);
13560            if backend.core.readiness.filter_interest().is_readable() {
13561                backend.readable(&mut context, EndpointClient(&mut router));
13562            }
13563        }
13564        backend.core.readiness.event.insert(Ready::WRITABLE);
13565        backend.writable(&mut context, EndpointClient(&mut router));
13566
13567        let frames = peer_frames(&backend.socket.wire).expect("whole frames on the wire");
13568        assert!(
13569            !frames.iter().any(|(kind, ..)| *kind == 7),
13570            "the backend connection must not answer a valid initial window with GOAWAY"
13571        );
13572        assert!(
13573            frames
13574                .iter()
13575                .any(|(kind, flags, ..)| *kind == 4 && *flags == 1),
13576            "the backend's SETTINGS are acknowledged"
13577        );
13578        assert!(
13579            frames.iter().any(|(kind, _, id, _)| *kind == 1 && *id == 1),
13580            "and the request leaves on stream 1"
13581        );
13582    }
13583
13584    /// A 4-octet WINDOW_UPDATE frame for `stream_id`.
13585    fn window_update_frame(stream_id: u32, increment: u32) -> Vec<u8> {
13586        orphan_frame(8, 0, stream_id, 4, &increment.to_be_bytes())
13587    }
13588
13589    /// RFC 9113 §6.9 on the backend leg: `start_stream` sizes the stream's
13590    /// send window from the initial window the BACKEND advertised (here
13591    /// 1000, already received on a pooled connection), whatever the frontend
13592    /// granted: larger (an H2 client at 2^31-1), the `1 << 16` an H1
13593    /// frontend seeds, or nothing. Taking the frontend's value would let a
13594    /// request body overrun the backend's window. The backend's WINDOW_UPDATE
13595    /// credits that leg only.
13596    ///
13597    /// TO SEE THIS RED: delete the `send_window_mut` assignment in
13598    /// `ConnectionH2::start_stream`: `the backend leg starts at the
13599    /// backend's window`, `left: 65535, right: 1000`. Verified 2026-10-01.
13600    #[test]
13601    fn a_backend_stream_window_is_sized_and_credited_by_the_backend_only() {
13602        const BACKEND_WINDOW: u32 = 1000;
13603        for front_window in [FLOW_CONTROL_MAX_WINDOW, 1 << 16, 0] {
13604            let pool = make_pool_for_invariant_16();
13605            let mut backend = paced_backend(&pool, usize::MAX);
13606            let mut context = test_context(&pool);
13607            let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
13608            let read = |backend: &mut H2Shell<PacedSocket>,
13609                        context: &mut Context<TestListener>,
13610                        router: &mut Router| {
13611                for _ in 0..4 {
13612                    backend.core.readiness.event.insert(Ready::READABLE);
13613                    if backend.core.readiness.filter_interest().is_readable() {
13614                        backend.readable(context, EndpointClient(router));
13615                    }
13616                }
13617            };
13618            let mut initial_window = vec![0, 4];
13619            initial_window.extend_from_slice(&BACKEND_WINDOW.to_be_bytes());
13620            backend
13621                .socket
13622                .inbound
13623                .extend(orphan_frame(4, 0, 0, 6, &initial_window));
13624            read(&mut backend, &mut context, &mut router);
13625            let gid = context
13626                .create_stream(Ulid::generate(), front_window)
13627                .expect("test context must create a stream");
13628            queue_backend_request(&mut context, gid, b"key");
13629            context.streams[gid].state = StreamState::Linked(mio::Token(1));
13630            assert!(backend.start_stream(gid, &mut context), "premise: stream 1");
13631            assert_eq!(
13632                context.streams[gid].back_window, BACKEND_WINDOW as i32,
13633                "front window {front_window}: the backend leg starts at the backend's window"
13634            );
13635
13636            backend.socket.inbound.extend(window_update_frame(1, 500));
13637            read(&mut backend, &mut context, &mut router);
13638            let stream = &context.streams[gid];
13639            assert_eq!(
13640                stream.back_window,
13641                BACKEND_WINDOW as i32 + 500,
13642                "front window {front_window}: the backend's WINDOW_UPDATE credits its leg"
13643            );
13644            assert_eq!(
13645                stream.front_window,
13646                i32::try_from(front_window).unwrap(),
13647                "front window {front_window}: and leaves the frontend leg alone"
13648            );
13649        }
13650    }
13651
13652    /// Feed `inbound` to a frontend connection carrying streams 1 and 3,
13653    /// flush what it answers, and return the GOAWAY error codes it wrote.
13654    fn goaway_codes_after(
13655        connection: &mut H2Shell<PacedSocket>,
13656        context: &mut Context<TestListener>,
13657        router: &mut Router,
13658        inbound: &[u8],
13659    ) -> Vec<u32> {
13660        connection.socket.inbound.extend(inbound);
13661        for _ in 0..4 {
13662            connection.core.readiness.event.insert(Ready::READABLE);
13663            if connection.core.readiness.filter_interest().is_readable() {
13664                connection.readable(context, EndpointClient(router));
13665            }
13666        }
13667        connection.core.readiness.event.insert(Ready::WRITABLE);
13668        connection.writable(context, EndpointClient(router));
13669        peer_frames(&connection.socket.wire)
13670            .expect("whole frames on the wire")
13671            .iter()
13672            .filter(|(kind, ..)| *kind == 7)
13673            .map(|(_, _, _, payload)| {
13674                u32::from_be_bytes(payload[4..8].try_into().expect("GOAWAY error code"))
13675            })
13676            .collect()
13677    }
13678
13679    /// A SETTINGS frame carrying only `SETTINGS_INITIAL_WINDOW_SIZE = value`.
13680    fn initial_window_settings(value: u32) -> Vec<u8> {
13681        let mut payload = vec![0, 4];
13682        payload.extend_from_slice(&value.to_be_bytes());
13683        orphan_frame(4, 0, 0, 6, &payload)
13684    }
13685
13686    /// RFC 9113 §6.5.2: a `SETTINGS_INITIAL_WINDOW_SIZE` above 2^31-1 "MUST
13687    /// be treated as a connection error of type FLOW_CONTROL_ERROR".
13688    ///
13689    /// TO SEE THIS RED: answer the `SETTINGS_INITIAL_WINDOW_SIZE` arm of
13690    /// `ConnectionH2::handle_settings_frame` with `H2Error::ProtocolError`:
13691    /// `left: [1], right: [3]`. Verified 2026-10-01 (red on `c8779209`).
13692    #[test]
13693    fn an_initial_window_above_the_maximum_is_a_flow_control_error() {
13694        let (_pool, mut connection, mut context, mut router, _peer) = two_requests_read(usize::MAX);
13695        let codes = goaway_codes_after(
13696            &mut connection,
13697            &mut context,
13698            &mut router,
13699            &initial_window_settings(FLOW_CONTROL_MAX_WINDOW + 1),
13700        );
13701        assert_eq!(codes, vec![0x3], "GOAWAY(FLOW_CONTROL_ERROR)");
13702    }
13703
13704    /// RFC 9113 §6.9.2: a SETTINGS change that pushes a stream window past
13705    /// 2^31-1 is a connection error of type FLOW_CONTROL_ERROR. Stream 3's
13706    /// window already stands at 2^31-1; raising the initial window by one
13707    /// overflows it. The rejection is checked before any window moves, so
13708    /// stream 1, walked first, keeps its window.
13709    ///
13710    /// TO SEE THIS RED: in `ConnectionH2::update_initial_window_size`,
13711    /// apply each delta as it is checked, returning on the first overflow:
13712    /// `the rejected setting changes no stream window`, `left: 65536, right:
13713    /// 65535`. Verified 2026-10-01 (red on `c8779209`).
13714    #[test]
13715    fn a_settings_change_overflowing_a_stream_window_is_a_flow_control_error() {
13716        let (_pool, mut connection, mut context, mut router, _peer) = two_requests_read(usize::MAX);
13717        let gid = |connection: &H2Shell<PacedSocket>, id: u32| {
13718            *connection
13719                .core
13720                .stream_table
13721                .streams()
13722                .get(&id)
13723                .expect("the stream is open")
13724        };
13725        let (first, third) = (gid(&connection, 1), gid(&connection, 3));
13726        let mut inbound = orphan_frame(
13727            8,
13728            0,
13729            3,
13730            4,
13731            &(FLOW_CONTROL_MAX_WINDOW - DEFAULT_INITIAL_WINDOW_SIZE).to_be_bytes(),
13732        );
13733        inbound.extend(initial_window_settings(DEFAULT_INITIAL_WINDOW_SIZE + 1));
13734        let codes = goaway_codes_after(&mut connection, &mut context, &mut router, &inbound);
13735        assert_eq!(
13736            context.streams[third].front_window, FLOW_CONTROL_MAX_WINDOW as i32,
13737            "premise: stream 3's window stands at 2^31-1"
13738        );
13739        assert_eq!(codes, vec![0x3], "GOAWAY(FLOW_CONTROL_ERROR)");
13740        assert_eq!(
13741            context.streams[first].front_window, DEFAULT_INITIAL_WINDOW_SIZE as i32,
13742            "the rejected setting changes no stream window"
13743        );
13744    }
13745
13746    /// RFC 9113 §6.9 on the frontend leg: the client's WINDOW_UPDATE and
13747    /// SETTINGS_INITIAL_WINDOW_SIZE move the stream's frontend window only.
13748    #[test]
13749    fn a_frontend_window_update_credits_the_frontend_leg_only() {
13750        let (_pool, mut connection, mut context, mut router, _peer) = two_requests_read(usize::MAX);
13751        let first = *connection
13752            .core
13753            .stream_table
13754            .streams()
13755            .get(&1)
13756            .expect("the stream is open");
13757        context.streams[first].back_window = 1234;
13758        let mut initial_window = vec![0, 4];
13759        initial_window.extend_from_slice(&100_000u32.to_be_bytes());
13760        connection
13761            .socket
13762            .inbound
13763            .extend(window_update_frame(1, 1000));
13764        connection
13765            .socket
13766            .inbound
13767            .extend(orphan_frame(4, 0, 0, 6, &initial_window));
13768        for _ in 0..4 {
13769            connection.core.readiness.event.insert(Ready::READABLE);
13770            if connection.core.readiness.filter_interest().is_readable() {
13771                connection.readable(&mut context, EndpointClient(&mut router));
13772            }
13773        }
13774        let stream = &context.streams[first];
13775        assert_eq!(
13776            stream.front_window,
13777            100_000 + 1000,
13778            "the client's WINDOW_UPDATE and initial window move the frontend leg"
13779        );
13780        assert_eq!(stream.back_window, 1234, "and not the backend leg");
13781    }
13782
13783    /// sozu-proxy/sozu#1632, GOAWAY form: the backend refuses the stream
13784    /// (`last_stream_id` below it) after its HEADERS were encoded but not
13785    /// sent. It was not processed, but its request cannot be sent anywhere
13786    /// else: `front.out` holds this connection's frames and the blocks are
13787    /// gone. It must be refused (REFUSED_STREAM toward the client, RFC 9113
13788    /// §8.7), never queued for a re-link.
13789    ///
13790    /// TO SEE THIS RED: in [`ConnectionH2::handle_goaway_frame`], test
13791    /// `stream.front.consumed` alone. The stream is then re-linked with its
13792    /// stale frames: `a request encoded for this backend must not be re-linked
13793    /// after its GOAWAY`. Red on `d5161919`, verified 2026-09-28.
13794    #[test]
13795    fn a_request_encoded_for_a_backend_is_refused_on_goaway_not_relinked() {
13796        let pool = make_pool_for_invariant_16();
13797        let mut backend = paced_backend(&pool, 0);
13798        let mut context = test_context(&pool);
13799        let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
13800        let (gid, _) = park_an_encoded_request(&mut backend, &mut context, &mut router);
13801
13802        // GOAWAY(last_stream_id = 0, NO_ERROR): stream 1 was not processed.
13803        backend
13804            .socket
13805            .inbound
13806            .extend(orphan_frame(7, 0, 0, 8, &[0; 8]));
13807        for _ in 0..4 {
13808            backend.core.readiness.event.insert(Ready::READABLE);
13809            if backend.core.readiness.filter_interest().is_readable() {
13810                backend.readable(&mut context, EndpointClient(&mut router));
13811            }
13812        }
13813
13814        assert!(
13815            !context.pending_links.contains(&gid),
13816            "a request encoded for this backend must not be re-linked after its GOAWAY"
13817        );
13818        let stream = &context.streams[gid];
13819        assert!(
13820            stream.context.status == Some(503) && stream.state == StreamState::Unlinked,
13821            "the client must be answered 503, got {:?} / {:?}",
13822            stream.context.status,
13823            stream.state
13824        );
13825    }
13826
13827    /// sozu-proxy/sozu#1632, H1 frontend form (found by the adversarial
13828    /// review of the first fix): behind a GOAWAY that refuses a request its
13829    /// H2 backend encoded but did not send, an H1 client must receive an
13830    /// error response, not silence. The first fix answered with
13831    /// `forcefully_terminate_answer(REFUSED_STREAM)`, an empty response kawa
13832    /// in `Error`, which `ConnectionH1::writable` cannot write: it withdraws
13833    /// WRITABLE and the client waits for its timeout.
13834    ///
13835    /// TO SEE THIS RED: in [`ConnectionH2::handle_goaway_frame`], answer the
13836    /// `front_bound_to_backend` branch with `forcefully_terminate_answer(..,
13837    /// H2Error::RefusedStream)` as the consumed branch does. The H1 client
13838    /// then reads nothing: `the H1 client must read an error response, got
13839    /// ""`. Verified 2026-09-28.
13840    #[test]
13841    fn an_h1_client_behind_a_goaway_refusing_its_encoded_request_gets_a_response() {
13842        use std::io::Read;
13843
13844        let pool = make_pool_for_invariant_16();
13845        let mut backend = paced_backend(&pool, 0);
13846        let mut context = test_context(&pool);
13847        let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
13848        let (gid, _) = park_an_encoded_request(&mut backend, &mut context, &mut router);
13849        let (socket, mut client) = connected_socket();
13850        let mut frontend =
13851            Connection::new_h1_server(Ulid::generate(), socket, Duration::from_secs(30));
13852        assert_eq!(gid, 0, "premise: the H1 frontend serves slot 0");
13853
13854        backend
13855            .socket
13856            .inbound
13857            .extend(orphan_frame(7, 0, 0, 8, &[0; 8]));
13858        for _ in 0..4 {
13859            backend.core.readiness.event.insert(Ready::READABLE);
13860            if backend.core.readiness.filter_interest().is_readable() {
13861                backend.readable(
13862                    &mut context,
13863                    crate::protocol::mux::connection::EndpointServer(&mut frontend),
13864                );
13865            }
13866        }
13867        assert!(
13868            !context.pending_links.contains(&gid),
13869            "premise: the stream was refused, not re-linked"
13870        );
13871
13872        let mut received = Vec::new();
13873        for _ in 0..16 {
13874            frontend.readiness_mut().event.insert(Ready::WRITABLE);
13875            if frontend.readiness().filter_interest().is_writable() {
13876                frontend.writable(&mut context, EndpointClient(&mut router));
13877            }
13878            let mut buf = [0u8; 4096];
13879            if let Ok(n) = client.read(&mut buf) {
13880                received.extend_from_slice(&buf[..n]);
13881            }
13882            std::thread::yield_now();
13883        }
13884        assert!(
13885            received.starts_with(b"HTTP/1.1 503"),
13886            "the H1 client must read an error response, got {:?}",
13887            String::from_utf8_lossy(&received)
13888        );
13889    }
13890
13891    /// sozu-proxy/sozu#1632, connection-loss form: the backend connection
13892    /// goes away with the request encoded and parked, and
13893    /// `H2Shell::close` hands the stream to its frontend, whose
13894    /// `end_stream_decision` used to answer `Reconnect` on
13895    /// `!front.consumed`. The request must be answered with an error instead.
13896    ///
13897    /// TO SEE THIS RED: in `shared::end_stream_decision`, delete the
13898    /// `stream.front_bound_to_backend` branch. The stream is then re-linked:
13899    /// `a request encoded for a lost backend must not be re-linked`. Red on
13900    /// `d5161919`, verified 2026-09-28.
13901    #[test]
13902    fn a_request_encoded_for_a_lost_backend_is_answered_not_relinked() {
13903        let pool = make_pool_for_invariant_16();
13904        let mut backend = paced_backend(&pool, 0);
13905        let mut context = test_context(&pool);
13906        let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
13907        let (gid, _) = park_an_encoded_request(&mut backend, &mut context, &mut router);
13908
13909        // What `H2Shell::close` does through `EndpointServer`: the frontend
13910        // ends the stream the lost backend carried.
13911        let (mut frontend, _peer) = test_h2_connection(&pool, None);
13912        frontend.core.end_stream(gid, &mut context);
13913
13914        assert!(
13915            !context.pending_links.contains(&gid),
13916            "a request encoded for a lost backend must not be re-linked"
13917        );
13918        assert_eq!(
13919            context.streams[gid].context.status,
13920            Some(502),
13921            "the client gets the answer a request that reached the backend gets"
13922        );
13923    }
13924
13925    /// sozu-proxy/sozu#1632, the property itself: no byte one backend
13926    /// connection's encoder produced may leave on another connection. Here
13927    /// the request encoded on backend A is offered to backend B, as a
13928    /// re-link would; B must refuse it and write nothing of A's.
13929    ///
13930    /// TO SEE THIS RED: in [`ConnectionH2::start_stream`], delete the
13931    /// `front.out` guard. B then sends A's frames — stream id 1 of A, a field
13932    /// block encoded against A's table: `backend B must not send what
13933    /// backend A encoded`. Red on `d5161919`, verified 2026-09-28.
13934    #[test]
13935    fn a_request_encoded_for_one_backend_never_reaches_another() {
13936        let pool = make_pool_for_invariant_16();
13937        let mut first = paced_backend(&pool, 0);
13938        let mut context = test_context(&pool);
13939        let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
13940        let (gid, encoded) = park_an_encoded_request(&mut first, &mut context, &mut router);
13941        assert!(
13942            peer_frames(&encoded)
13943                .expect("whole frames")
13944                .iter()
13945                .any(|(kind, _, id, _)| *kind == 1 && *id == 1),
13946            "premise: backend A encoded a HEADERS frame for its stream 1"
13947        );
13948
13949        let mut second = paced_backend(&pool, usize::MAX);
13950        let started = second.start_stream(gid, &mut context);
13951        drive_both_ways(&mut second, &mut context, &mut router);
13952
13953        assert!(
13954            !second
13955                .socket
13956                .wire
13957                .windows(encoded.len())
13958                .any(|window| window == encoded.as_slice()),
13959            "backend B must not send what backend A encoded: {:?}",
13960            peer_frames(&second.socket.wire)
13961        );
13962        assert!(
13963            !started,
13964            "backend B must refuse a request encoded for another connection"
13965        );
13966    }
13967
13968    /// Queue on `gid`'s response side a 200 whose field block is `fields`,
13969    /// closed with END_STREAM, the way a relayed response reaches the write
13970    /// pass.
13971    fn queue_response(
13972        connection: &mut H2Shell<PacedSocket>,
13973        context: &mut Context<TestListener>,
13974        gid: GlobalStreamId,
13975        fields: Vec<(&'static [u8], kawa::Store)>,
13976    ) {
13977        let stream = &mut context.streams[gid];
13978        let kawa = &mut stream.back;
13979        kawa.detached.status_line = kawa::StatusLine::Response {
13980            version: kawa::Version::V20,
13981            code: 200,
13982            status: kawa::Store::Static(b"200"),
13983            reason: kawa::Store::Empty,
13984        };
13985        kawa.push_block(kawa::Block::StatusLine);
13986        for (name, value) in fields {
13987            kawa.push_block(kawa::Block::Header(kawa::Pair {
13988                key: kawa::Store::Static(name),
13989                val: value,
13990            }));
13991        }
13992        kawa.push_block(kawa::Block::Flags(kawa::Flags {
13993            end_body: false,
13994            end_chunk: false,
13995            end_header: true,
13996            end_stream: true,
13997        }));
13998        kawa.parsing_phase = kawa::ParsingPhase::Terminated;
13999        stream.state = StreamState::Unlinked;
14000        connection.core.readiness.arm_writable();
14001    }
14002
14003    /// A block dropped for exceeding `MAX_HEADER_LIST_SIZE` re-arms a table
14004    /// reset that must outlive the write pass when no other block of that
14005    /// pass carries it (sozu-proxy/sozu#1627, path 2). Stream 1's response
14006    /// inserts `x-a: 1`, then an `etag` value past the limit aborts it into a
14007    /// RST_STREAM, and it is the only header block of its pass. Stream 3's
14008    /// response, answered afterwards, encodes `x-a: 1` again, and the peer
14009    /// must read it: its block has to open with the reset.
14010    ///
14011    /// TO SEE THIS RED: in `ConnectionH2::poll_write_target`'s
14012    /// `H2WritePhase::End` arm, restore the pre-image
14013    /// `if converter_pass.size_update_emitted() { self.pending_table_size_update
14014    /// = None; }` instead of taking `converter_pass.pending_table_size_update()`.
14015    /// The reset is then lost with the pass: `the reset is still owed after
14016    /// the pass`, `left: None, right: Some(4096)`. Verified 2026-09-27.
14017    #[test]
14018    fn an_oversized_block_reset_outlives_the_pass_that_dropped_it() {
14019        let (_pool, mut connection, mut context, mut router, _peer) = two_requests_read(usize::MAX);
14020        let gid = |connection: &H2Shell<PacedSocket>, id: u32| {
14021            *connection
14022                .core
14023                .stream_table
14024                .streams()
14025                .get(&id)
14026                .expect("the stream is open")
14027        };
14028        let (first, third) = (gid(&connection, 1), gid(&connection, 3));
14029        let oversized = vec![b'v'; MAX_HEADER_LIST_SIZE + 1];
14030        queue_response(
14031            &mut connection,
14032            &mut context,
14033            first,
14034            vec![
14035                (b"x-a", kawa::Store::Static(b"1")),
14036                (b"etag", kawa::Store::from_slice(&oversized)),
14037            ],
14038        );
14039        drive_both_ways(&mut connection, &mut context, &mut router);
14040        assert!(
14041            peer_frames(&connection.socket.wire)
14042                .unwrap()
14043                .iter()
14044                .any(|(kind, _, id, _)| *kind == 3 && *id == 1),
14045            "premise: stream 1 was reset without HEADERS"
14046        );
14047        assert_eq!(
14048            connection.core.pending_table_size_update,
14049            Some(4096),
14050            "the reset is still owed after the pass"
14051        );
14052        queue_response(
14053            &mut connection,
14054            &mut context,
14055            third,
14056            vec![(b"x-a", kawa::Store::Static(b"1"))],
14057        );
14058        drive_both_ways(&mut connection, &mut context, &mut router);
14059        let (seen, status) = peer_header_blocks(&connection.socket.wire);
14060        assert_eq!(
14061            seen,
14062            vec![(3, true)],
14063            "the peer must decode stream 3's block with its own table"
14064        );
14065        assert_eq!(status, vec![b"200".to_vec()]);
14066    }
14067
14068    /// A response body 1000 octets past the default window, so one prepare
14069    /// takes the whole connection window and leaves the rest in the blocks.
14070    /// A `Store::Static`, because kawa 0.7.1's `Store::Alloc` arm of
14071    /// `Store::consume` subtracts the length before it adds the index back,
14072    /// which overflows in a debug build once the converter has split the
14073    /// chunk (CleverCloud/kawa#22).
14074    static WINDOW_SIZED_BODY: [u8; DEFAULT_INITIAL_WINDOW_SIZE as usize + 1000] =
14075        [b'b'; DEFAULT_INITIAL_WINDOW_SIZE as usize + 1000];
14076
14077    /// Queue on `gid`'s response side a 200 carrying `body`, closed with
14078    /// END_STREAM, the way a relayed response with a payload reaches the
14079    /// write pass.
14080    fn queue_response_with_body(
14081        connection: &mut H2Shell<PacedSocket>,
14082        context: &mut Context<TestListener>,
14083        gid: GlobalStreamId,
14084        body: &'static [u8],
14085    ) {
14086        let stream = &mut context.streams[gid];
14087        let kawa = &mut stream.back;
14088        kawa.detached.status_line = kawa::StatusLine::Response {
14089            version: kawa::Version::V20,
14090            code: 200,
14091            status: kawa::Store::Static(b"200"),
14092            reason: kawa::Store::Empty,
14093        };
14094        kawa.push_block(kawa::Block::StatusLine);
14095        kawa.push_block(kawa::Block::Flags(kawa::Flags {
14096            end_body: false,
14097            end_chunk: false,
14098            end_header: true,
14099            end_stream: false,
14100        }));
14101        kawa.push_block(kawa::Block::Chunk(kawa::Chunk {
14102            data: kawa::Store::Static(body),
14103        }));
14104        kawa.push_block(kawa::Block::Flags(kawa::Flags {
14105            end_body: true,
14106            end_chunk: false,
14107            end_header: false,
14108            end_stream: true,
14109        }));
14110        kawa.parsing_phase = kawa::ParsingPhase::Terminated;
14111        stream.state = StreamState::Unlinked;
14112        connection.core.readiness.arm_writable();
14113    }
14114
14115    /// The DATA payload octets `wire` carries for `stream_id`.
14116    fn data_on_wire(wire: &[u8], stream_id: u32) -> usize {
14117        peer_frames(wire)
14118            .unwrap()
14119            .iter()
14120            .filter(|(kind, _, id, _)| *kind == 0 && *id == stream_id)
14121            .map(|(_, _, _, payload)| payload.len())
14122            .sum()
14123    }
14124
14125    /// Stream 1's response body fills the whole connection send window in
14126    /// one prepare, the socket takes `budget` octets, and the stream is
14127    /// parked with the rest still queued. Its removal drops what the wire
14128    /// has not started.
14129    fn park_a_window_sized_body_on_stream_1(
14130        budget: usize,
14131    ) -> (
14132        H2Shell<PacedSocket>,
14133        Context<TestListener>,
14134        Router,
14135        GlobalStreamId,
14136        GlobalStreamId,
14137    ) {
14138        let (_pool, mut connection, mut context, mut router, _peer) = two_requests_read(budget);
14139        let gid = |connection: &H2Shell<PacedSocket>, id: u32| {
14140            *connection
14141                .core
14142                .stream_table
14143                .streams()
14144                .get(&id)
14145                .expect("the stream is open")
14146        };
14147        let (first, third) = (gid(&connection, 1), gid(&connection, 3));
14148        queue_response_with_body(&mut connection, &mut context, first, &WINDOW_SIZED_BODY);
14149        connection.core.readiness.event.insert(Ready::WRITABLE);
14150        connection.writable(&mut context, EndpointClient(&mut router));
14151        assert!(
14152            connection.socket.wire.len() == budget
14153                && connection.core.stream_table.expect_write().is_some(),
14154            "premise: stream 1's DATA was prepared and parked, the socket took {budget} octets"
14155        );
14156        assert_eq!(
14157            connection.core.flow_control.window(),
14158            0,
14159            "premise: preparing stream 1's body took the whole connection window"
14160        );
14161        (connection, context, router, first, third)
14162    }
14163
14164    /// RFC 9113 §6.9: the connection send window accounts for the DATA that
14165    /// was SENT. DATA prepared on stream 1 and dropped unsent when the peer
14166    /// resets it never reaches the peer, which will therefore never return
14167    /// that credit, so the connection must take it back itself; otherwise
14168    /// the window stays at zero for the life of the connection and stream 3's
14169    /// body never leaves (sozu-proxy/sozu#1641).
14170    ///
14171    /// TO SEE THIS RED: in `ConnectionH2::remove_dead_stream`, delete the
14172    /// `self.refund_parked_data(parked_data)` call. The removal then gives
14173    /// nothing back: `the removal itself gives the credit back`, `left: 0,
14174    /// right: 65535`. Without the fix at all, stream 3 also sends nothing
14175    /// (`left: 0, right: 1000`). Delete the `self.readiness.arm_writable()`
14176    /// in `ConnectionH2::refund_parked_data` instead: `and re-arms WRITABLE`
14177    /// fails. Verified 2026-09-28 (red on `5db53c60`).
14178    #[test]
14179    fn data_dropped_with_its_parked_stream_gives_the_connection_window_back() {
14180        let (mut connection, mut context, mut router, _first, third) =
14181            park_a_window_sized_body_on_stream_1(0);
14182        // A zero-progress pass may release WRITABLE; only the refund may
14183        // bring it back here.
14184        connection.core.readiness.interest.remove(Ready::WRITABLE);
14185        connection.socket.inbound.extend(RST_STREAM_1);
14186        for _ in 0..4 {
14187            connection.core.readiness.event.insert(Ready::READABLE);
14188            connection.readable(&mut context, EndpointClient(&mut router));
14189        }
14190        assert!(
14191            !connection.core.stream_table.streams().contains_key(&1),
14192            "premise: the RST removed stream 1"
14193        );
14194        assert_eq!(
14195            connection.core.flow_control.window(),
14196            DEFAULT_INITIAL_WINDOW_SIZE as i32,
14197            "the removal itself gives the credit back, before any write pass"
14198        );
14199        assert!(
14200            connection.core.readiness.interest.is_writable(),
14201            "and re-arms WRITABLE for the streams the closed window starved"
14202        );
14203        connection.socket.budget = usize::MAX;
14204        queue_response_with_body(&mut connection, &mut context, third, &[b'c'; 1000]);
14205        drive_both_ways(&mut connection, &mut context, &mut router);
14206        let wire = &connection.socket.wire;
14207        assert_eq!(data_on_wire(wire, 1), 0, "stream 1's DATA never left");
14208        assert_eq!(
14209            data_on_wire(wire, 3),
14210            1000,
14211            "stream 3's body must not wait on credit spent on dropped DATA"
14212        );
14213        assert_eq!(
14214            i64::from(connection.core.flow_control.window())
14215                + (data_on_wire(wire, 1) + data_on_wire(wire, 3)) as i64,
14216            i64::from(DEFAULT_INITIAL_WINDOW_SIZE),
14217            "the window is debited for the DATA octets sent, and only those"
14218        );
14219    }
14220
14221    /// The same drop without a removal: a forced termination clears the
14222    /// parked stream's queue and keeps the stream, which then sends its
14223    /// RST_STREAM. Both the connection's and the stream's credit for the
14224    /// dropped DATA come back (sozu-proxy/sozu#1641).
14225    ///
14226    /// TO SEE THIS RED: in `ConnectionH2::poll_write_target`'s
14227    /// `H2WritePhase::Start` arm, delete the `self.refund_parked_data(dropped)`
14228    /// call: `the connection window is back where it was before the
14229    /// prepare`, `left: 0, right: 65535`. Delete the `*parts.window` refund
14230    /// beside it instead: `the stream's window is back where it was before
14231    /// the prepare`, `left: 0, right: 65535`. Verified 2026-09-28 (red on
14232    /// `5db53c60`).
14233    #[test]
14234    fn data_dropped_from_a_live_parked_stream_gives_both_windows_back() {
14235        let (mut connection, mut context, mut router, first, _third) =
14236            park_a_window_sized_body_on_stream_1(0);
14237        crate::protocol::mux::answers::forcefully_terminate_answer(
14238            &mut context.streams[first],
14239            &mut connection.core.readiness,
14240            H2Error::InternalError,
14241        );
14242        connection.socket.budget = usize::MAX;
14243        connection.core.readiness.event.insert(Ready::WRITABLE);
14244        connection.writable(&mut context, EndpointClient(&mut router));
14245        assert_eq!(
14246            context.streams[first].front_window, DEFAULT_INITIAL_WINDOW_SIZE as i32,
14247            "the stream's window is back where it was before the prepare"
14248        );
14249        drive_both_ways(&mut connection, &mut context, &mut router);
14250        assert_eq!(
14251            data_on_wire(&connection.socket.wire, 1),
14252            0,
14253            "premise: stream 1's DATA never left"
14254        );
14255        assert_eq!(
14256            connection.core.flow_control.window(),
14257            DEFAULT_INITIAL_WINDOW_SIZE as i32,
14258            "the connection window is back where it was before the prepare"
14259        );
14260    }
14261
14262    /// The peer grows the connection window to 2^31-1 while stream 1's
14263    /// 65535 prepared octets sit parked, then resets the stream. The window
14264    /// that really stands is 2^31-1 plus those octets, past the RFC 9113
14265    /// §6.9.1 ceiling, and the refund is where that shows: the connection
14266    /// answers GOAWAY(FLOW_CONTROL_ERROR), as it does for an overflowing
14267    /// WINDOW_UPDATE, instead of asserting on what the peer sent.
14268    ///
14269    /// TO SEE THIS RED: in `H2FlowControl::refund_send_window`, put back a
14270    /// `debug_assert!(false, ..)` on the `checked_add` overflow: the test
14271    /// panics on `a refund of consumed credit cannot overflow the send
14272    /// window`, which is how `08730c9a` behaved. In
14273    /// `ConnectionH2::refund_parked_data`, answer `None` on the overflow
14274    /// instead of the GOAWAY: `exactly one GOAWAY`, `left: 0, right: 1`.
14275    /// Verified 2026-09-28.
14276    #[test]
14277    fn a_refund_the_peer_pushed_past_the_window_ceiling_is_a_flow_control_error() {
14278        let (mut connection, mut context, mut router, _first, _third) =
14279            park_a_window_sized_body_on_stream_1(0);
14280        // WINDOW_UPDATE(stream 0, 2^31-1), then RST_STREAM(1, CANCEL).
14281        connection
14282            .socket
14283            .inbound
14284            .extend(b"\x00\x00\x04\x08\x00\x00\x00\x00\x00\x7f\xff\xff\xff");
14285        connection.socket.inbound.extend(RST_STREAM_1);
14286        for _ in 0..4 {
14287            connection.core.readiness.event.insert(Ready::READABLE);
14288            connection.readable(&mut context, EndpointClient(&mut router));
14289        }
14290        assert_eq!(
14291            connection.core.flow_control.window(),
14292            i32::MAX,
14293            "premise: the peer's WINDOW_UPDATE was accepted, and the refund not applied"
14294        );
14295        connection.socket.budget = usize::MAX;
14296        drive_both_ways(&mut connection, &mut context, &mut router);
14297        let goaways: Vec<Vec<u8>> = peer_frames(&connection.socket.wire)
14298            .unwrap()
14299            .into_iter()
14300            .filter(|(kind, _, _, _)| *kind == 7)
14301            .map(|(_, _, _, payload)| payload)
14302            .collect();
14303        assert_eq!(goaways.len(), 1, "exactly one GOAWAY");
14304        assert_eq!(
14305            &goaways[0][4..8],
14306            &3u32.to_be_bytes(),
14307            "the GOAWAY carries FLOW_CONTROL_ERROR"
14308        );
14309    }
14310
14311    /// A second write pass that is stalled too resumes the parked stream,
14312    /// writes nothing and parks it again: every prepared DATA octet is still
14313    /// queued, so nothing is given back and both windows stay at zero
14314    /// (sozu-proxy/sozu#1641). Only DATA no longer queued is refunded.
14315    ///
14316    /// TO SEE THIS RED: in `ConnectionH2::poll_write_target`'s
14317    /// `H2WritePhase::Start` arm, refund `parked_data` instead of
14318    /// `parked_data - queued`: `left: 65535, right: 0`. Verified 2026-09-28.
14319    #[test]
14320    fn data_still_parked_after_a_second_stalled_pass_is_not_given_back() {
14321        let (mut connection, mut context, mut router, first, _third) =
14322            park_a_window_sized_body_on_stream_1(0);
14323        connection.core.readiness.event.insert(Ready::WRITABLE);
14324        connection.writable(&mut context, EndpointClient(&mut router));
14325        assert!(
14326            connection.socket.wire.is_empty()
14327                && connection.core.stream_table.expect_write().is_some(),
14328            "premise: the second pass resumed nothing and kept the park"
14329        );
14330        assert_eq!(
14331            connection.core.flow_control.window(),
14332            0,
14333            "the connection window still owes every queued DATA octet"
14334        );
14335        assert_eq!(
14336            context.streams[first].front_window, 0,
14337            "and so does the stream's"
14338        );
14339    }
14340
14341    /// A write that cuts stream 1's first DATA frame hands the rest of that
14342    /// frame to the ordered output queue, which sends it whole even after
14343    /// the stream is gone (sozu-proxy/sozu#1625). Only the frames behind it,
14344    /// which the wire never started, are given back: the window still
14345    /// accounts for exactly the DATA octets sent (sozu-proxy/sozu#1641).
14346    ///
14347    /// TO SEE THIS RED: in `ConnectionH2::remove_dead_stream`, give back the
14348    /// whole prepare instead of the parked DATA
14349    /// (`self.refund_parked_data(DEFAULT_INITIAL_WINDOW_SIZE as i32)`). The
14350    /// window then exceeds the peer's grant by the cut frame's payload:
14351    /// `left: 81919, right: 65535`. Without the fix at all it falls short by
14352    /// the dropped frames instead: `left: 16384, right: 65535`. Verified
14353    /// 2026-09-28 (red on `5db53c60`).
14354    #[test]
14355    fn data_a_partial_write_started_is_sent_whole_and_not_given_back() {
14356        // A 10-octet HEADERS (`:status: 200`), then 109 octets of the first
14357        // DATA frame: its header and 100 octets of payload.
14358        let budget = 10 + 9 + 100;
14359        let (mut connection, mut context, mut router, _first, _third) =
14360            park_a_window_sized_body_on_stream_1(budget);
14361        connection.socket.inbound.extend(RST_STREAM_1);
14362        for _ in 0..4 {
14363            connection.core.readiness.event.insert(Ready::READABLE);
14364            connection.readable(&mut context, EndpointClient(&mut router));
14365        }
14366        assert!(
14367            !connection.core.stream_table.streams().contains_key(&1),
14368            "premise: the RST removed stream 1"
14369        );
14370        connection.socket.budget = usize::MAX;
14371        drive_both_ways(&mut connection, &mut context, &mut router);
14372        let sent = data_on_wire(&connection.socket.wire, 1);
14373        assert_eq!(
14374            sent, 16_384,
14375            "the cut frame left whole, and nothing after it"
14376        );
14377        assert_eq!(
14378            i64::from(connection.core.flow_control.window()) + sent as i64,
14379            i64::from(DEFAULT_INITIAL_WINDOW_SIZE),
14380            "the window is debited for the DATA octets sent, and only those"
14381        );
14382    }
14383
14384    /// An H1 backend answers chunked with `Connection: close`, sends the
14385    /// last chunk and a first trailer line, then closes before the final
14386    /// CRLF. The trailer block can never be completed, and the converter
14387    /// defers a field block until its closing flags are queued
14388    /// (sozu-proxy/sozu#1627). `ConnectionH2::end_stream`'s
14389    /// `EndStreamAction::CloseDelimited` arm must not leave the stream
14390    /// waiting on that block: the chunked body was cut short, so it ends
14391    /// with RST_STREAM, exactly as `ConnectionH1::terminate_close_delimited`
14392    /// (`lib/src/protocol/mux/h1.rs`) does, and the unfinished trailer is
14393    /// dropped unencoded, so the next response still decodes on the peer.
14394    ///
14395    /// Red on `d12c3299`, which deferred the trailer with no way out: the
14396    /// stream stayed open with its trailer queued forever, `stream 1 must end
14397    /// with RST_STREAM`, frames `[(1, 0), (0, 0)]`. TO SEE THIS RED now: in
14398    /// `ConnectionH2::end_stream`, delete the `EndStreamAction::CloseDelimited`
14399    /// arm guarded by `body_size != kawa::BodySize::Empty`. The truncated
14400    /// response then ends silently: frames `[(1, 0), (0, 0), (0, 1)]`.
14401    /// Verified 2026-09-27.
14402    #[test]
14403    fn a_chunked_response_closed_inside_its_trailers_ends_with_a_reset() {
14404        let (_pool, mut connection, mut context, mut router, _peer) = two_requests_read(usize::MAX);
14405        let gid = |connection: &H2Shell<PacedSocket>, id: u32| {
14406            *connection
14407                .core
14408                .stream_table
14409                .streams()
14410                .get(&id)
14411                .expect("the stream is open")
14412        };
14413        let (first, third) = (gid(&connection, 1), gid(&connection, 3));
14414        {
14415            let stream = &mut context.streams[first];
14416            stream.context.keep_alive_backend = false;
14417            let bytes: &[u8] = b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n3\r\nabc\r\n0\r\nx-checksum: abc\r\n";
14418            let kawa = &mut stream.back;
14419            kawa.storage.space()[..bytes.len()].copy_from_slice(bytes);
14420            kawa.storage.fill(bytes.len());
14421            kawa::h1::parse(kawa, &mut kawa::h1::NoCallbacks);
14422            assert_eq!(
14423                kawa.parsing_phase,
14424                kawa::ParsingPhase::Trailers,
14425                "premise: the backend stopped inside the trailers"
14426            );
14427            stream.state = StreamState::Unlinked;
14428        }
14429        connection.core.readiness.arm_writable();
14430        drive_both_ways(&mut connection, &mut context, &mut router);
14431        connection.core.end_stream(first, &mut context);
14432        drive_both_ways(&mut connection, &mut context, &mut router);
14433
14434        let frames: Vec<(u8, u8)> = peer_frames(&connection.socket.wire)
14435            .unwrap()
14436            .iter()
14437            .filter(|(_, _, id, _)| *id == 1)
14438            .map(|(kind, flags, _, _)| (*kind, flags & parser::FLAG_END_STREAM))
14439            .collect();
14440        assert_eq!(
14441            frames.last().map(|(kind, _)| *kind),
14442            Some(0x3),
14443            "stream 1 must end with RST_STREAM, frames {frames:?}"
14444        );
14445        assert!(
14446            !frames.iter().any(|(_, end_stream)| *end_stream != 0),
14447            "a truncated chunked body never ends cleanly: {frames:?}"
14448        );
14449        assert!(
14450            !connection.core.stream_table.streams().contains_key(&1),
14451            "stream 1 is retired"
14452        );
14453
14454        queue_response(
14455            &mut connection,
14456            &mut context,
14457            third,
14458            vec![(b"x-checksum", kawa::Store::Static(b"abc"))],
14459        );
14460        drive_both_ways(&mut connection, &mut context, &mut router);
14461        let (seen, status) = peer_header_blocks(&connection.socket.wire);
14462        assert_eq!(
14463            seen,
14464            vec![(1, true), (3, true)],
14465            "every block the peer reads decodes with its own table"
14466        );
14467        assert_eq!(status, vec![b"200".to_vec(), b"200".to_vec()]);
14468    }
14469
14470    /// An H1 backend answers `Content-Length: 10` with `Connection: close`,
14471    /// sends 4 body bytes, and its connection goes away without the EOF read
14472    /// path ending the body (a socket error or a backend timeout), so
14473    /// `ConnectionH2::end_stream` meets the response still in its body phase.
14474    /// Its `EndStreamAction::CloseDelimited` arm must end the stream with
14475    /// RST_STREAM: the body is 6 bytes short of its `content-length`, and
14476    /// only a body with neither `Content-Length` nor chunked coding is
14477    /// delimited by the close (RFC 9112 §6.3; RFC 9113 §8.1.1).
14478    ///
14479    /// Red on `d5161919`, whose guard covered chunked only: stream 1 ended
14480    /// with DATA carrying END_STREAM (sozu-proxy/sozu#1633).
14481    #[test]
14482    fn a_content_length_response_cut_short_ends_with_a_reset() {
14483        let (_pool, mut connection, mut context, mut router, _peer) = two_requests_read(usize::MAX);
14484        let first = *connection
14485            .core
14486            .stream_table
14487            .streams()
14488            .get(&1)
14489            .expect("the stream is open");
14490        {
14491            let stream = &mut context.streams[first];
14492            stream.context.keep_alive_backend = false;
14493            let bytes: &[u8] = b"HTTP/1.1 200 OK\r\nContent-Length: 10\r\n\r\nabcd";
14494            let kawa = &mut stream.back;
14495            kawa.storage.space()[..bytes.len()].copy_from_slice(bytes);
14496            kawa.storage.fill(bytes.len());
14497            kawa::h1::parse(kawa, &mut kawa::h1::NoCallbacks);
14498            assert!(
14499                kawa.is_main_phase() && !kawa.is_terminated() && kawa.expects == 6,
14500                "premise: the body is 6 bytes short, {:?}",
14501                kawa.parsing_phase
14502            );
14503            stream.state = StreamState::Unlinked;
14504        }
14505        connection.core.readiness.arm_writable();
14506        drive_both_ways(&mut connection, &mut context, &mut router);
14507        connection.core.end_stream(first, &mut context);
14508        drive_both_ways(&mut connection, &mut context, &mut router);
14509
14510        let frames: Vec<(u8, u8)> = peer_frames(&connection.socket.wire)
14511            .unwrap()
14512            .iter()
14513            .filter(|(_, _, id, _)| *id == 1)
14514            .map(|(kind, flags, _, _)| (*kind, flags & parser::FLAG_END_STREAM))
14515            .collect();
14516        assert_eq!(
14517            frames.last().map(|(kind, _)| *kind),
14518            Some(0x3),
14519            "stream 1 must end with RST_STREAM, frames {frames:?}"
14520        );
14521        assert!(
14522            !frames.iter().any(|(_, end_stream)| *end_stream != 0),
14523            "a truncated Content-Length body never ends cleanly: {frames:?}"
14524        );
14525    }
14526
14527    /// The same block dropped while its stream stays parked:
14528    /// `forcefully_terminate_answer` (a backend that closed mid-response)
14529    /// clears the parked stream's `kawa.out`, which held the encoded block.
14530    /// Stream 1 then ends with a RST_STREAM, and stream 3's block must still
14531    /// decode on the peer (sozu-proxy/sozu#1627).
14532    ///
14533    /// TO SEE THIS RED: in `ConnectionH2::poll_write_target`'s
14534    /// `H2WritePhase::Start` arm, append `&& false` to the `.is_empty()` that
14535    /// judges a park still in place. The peer then fails on stream 3's block:
14536    /// `left: [(3, false)], right: [(3, true)]`, and this is the only test
14537    /// that fails. Verified 2026-09-27.
14538    #[test]
14539    fn a_header_block_cleared_from_a_parked_stream_keeps_the_peer_table_in_sync() {
14540        let (seen, status) =
14541            peer_reads_after_a_dropped_parked_block(|connection, context, _router| {
14542                let gid = *connection
14543                    .core
14544                    .stream_table
14545                    .streams()
14546                    .get(&1)
14547                    .expect("stream 1 is open");
14548                forcefully_terminate_answer(
14549                    &mut context.streams[gid],
14550                    &mut connection.core.readiness,
14551                    H2Error::InternalError,
14552                );
14553                assert!(
14554                    context.streams[gid].back.out.is_empty()
14555                        && connection.core.stream_table.expect_write().is_some(),
14556                    "premise: stream 1 stays parked with its block cleared"
14557                );
14558            });
14559        assert_eq!(
14560            seen,
14561            vec![(3, true)],
14562            "the peer must decode stream 3's block with its own table"
14563        );
14564        assert_eq!(status, vec![b"404".to_vec()], "and read what was encoded");
14565    }
14566
14567    /// Resume-path bytes are not pass progress.
14568    ///
14569    /// `finalize_write` reads `bytes_written_this_pass > 0` as
14570    /// `made_progress`, and `made_progress && any_pending_back()` is LIFECYCLE
14571    /// §9 invariant 16: it RETAINS `Ready::WRITABLE` because a pass that moved
14572    /// bytes and left blocks behind is a voluntary scheduler yield that will
14573    /// resume itself. The resume path's drain is not that. It happens in
14574    /// `H2WritePhase::Resume`, before the scheduler pass opens at all; it is a
14575    /// socket-backpressure catch-up rather than a
14576    /// yield, and the stranded blocks it leaves belong to a stream the
14577    /// converter never reached this pass. Counting it as progress would keep
14578    /// WRITABLE armed on a connection whose scheduler pass did nothing, and
14579    /// the next tick would find the same nothing — a spin, not a resume.
14580    ///
14581    /// So `resume_bytes` is deliberately NOT accumulated into
14582    /// `total_bytes_written`: only the main loop's `stream_bytes` is. With a
14583    /// drained resume and a zero-byte scheduler pass, the correct answer is
14584    /// `FinalizeAction::Quiesce`, which withdraws `Ready::WRITABLE` and waits
14585    /// for an external trigger.
14586    ///
14587    /// TO SEE THIS RED: in `begin_scheduler_pass`, seed the pass total from
14588    /// the resume counter — insert `pass.total_bytes_written =
14589    /// pass.resume_bytes;` immediately before
14590    /// `pass.adopt_scheduler_pass(converter_pass, order, census);`, which is
14591    /// where the pre-image seeded `let mut total_bytes_written: usize = 0;`
14592    /// ahead of its per-stream loop. Both counters are fields of
14593    /// `H2WritePass` and keeping them apart is the whole point, so the shape
14594    /// a refactor unifying the two flush paths would reach for is exactly this
14595    /// one statement. Measured: this test fails on its own first
14596    /// assertion, `a pass whose scheduler loop wrote nothing must withdraw
14597    /// WRITABLE interest`, with `left: Writable | Error | Hup, right: Error |
14598    /// Hup`, and it is the ONLY failure in the suite.
14599    /// No other test reaches the resume path at all, which is why the blast
14600    /// radius is one.
14601    #[test]
14602    fn resume_path_bytes_do_not_make_the_pass_progress() {
14603        let pool = make_pool_for_invariant_16();
14604        let (mut connection, mut context, gid, _peer) = writable_fixture(
14605            &pool,
14606            &[FIRST_BLOCK, SECOND_BLOCK],
14607            &[(TOTAL_QUEUED, SocketResult::Continue)],
14608        );
14609        let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
14610
14611        connection
14612            .core
14613            .stream_table
14614            .set_expect_write(Some(H2StreamId::Other {
14615                id: REGISTERED_STREAM_ID,
14616                gid,
14617            }));
14618        // A pending back-buffer, which is the OTHER half of invariant 16's
14619        // condition. The flush drains `out` and never touches
14620        // `blocks`, so this block survives the resume and makes
14621        // `any_stream_has_pending_back` answer true — leaving `made_progress`
14622        // as the single input that decides Quiesce against RetainPendingBack.
14623        context.streams[gid]
14624            .back
14625            .blocks
14626            .push_back(kawa::Block::StatusLine);
14627        assert_prepare_gate_is_shut(&context, gid);
14628        assert!(
14629            connection.core.readiness.interest.is_writable(),
14630            "premise: WRITABLE interest must start armed, or the withdrawal \
14631             below is unobservable, got {:?}",
14632            connection.core.readiness
14633        );
14634
14635        let result = connection.writable(&mut context, EndpointClient(&mut router));
14636
14637        assert_eq!(
14638            connection.core.readiness.interest,
14639            Ready::HUP | Ready::ERROR,
14640            "a pass whose scheduler loop wrote nothing must withdraw WRITABLE \
14641             interest: the resume path's drain is socket catch-up, not the \
14642             voluntary yield invariant 16 retains the bit for"
14643        );
14644        assert_eq!(
14645            context.streams[gid].metrics.bout, TOTAL_QUEUED,
14646            "premise: the resume path really did move every queued byte — \
14647             without that this test would assert Quiesce over a pass that \
14648             simply had nothing to write"
14649        );
14650        assert!(
14651            !context.streams[gid].back.blocks.is_empty(),
14652            "premise: the pending back-buffer must survive the pass, or \
14653             any_stream_has_pending_back answers false and RetainPendingBack \
14654             is unreachable whatever made_progress says"
14655        );
14656        assert_eq!(
14657            connection.core.stream_table.expect_write(),
14658            None,
14659            "a drained resume clears the park before the scheduler pass"
14660        );
14661        assert_eq!(
14662            connection.socket.vectored_calls, 1,
14663            "the scheduler pass must find nothing to write: the resume drained \
14664             `out`, and the prepare gate is shut"
14665        );
14666        assert!(
14667            matches!(result, MuxResult::Continue),
14668            "a quiesced write pass continues, got {result:?}"
14669        );
14670    }
14671
14672    // ── One pass, one prepare per stream ────────────────────────────────
14673    //
14674    // The four tests above pin the FLUSH half of a write pass. This one pins
14675    // the PREPARE half against the same refactor: `write_streams` runs the
14676    // gate at `if kawa.is_main_phase() || ...` — `kawa.prepare`, the
14677    // `*parts.window` debit, `flow_control.consume_send_window`,
14678    // `census.note_fired` and the `freshly_emitted_rsts` push — ONCE per
14679    // stream per pass, in `H2WritePhase::Prepare` and outside the round-again
14680    // `H2WritePhase::Flush` re-enters. A restructure that lifted those rounds
14681    // up beside the prepare would debit the send window twice for
14682    // one stream's single scheduling turn: a connection would believe it had
14683    // spent credit it never put on the wire, and would stall itself short of
14684    // the peer's real window.
14685    //
14686    // MEASURED, and it corrects the obvious way to write this test. A narrow
14687    // per-stream window does NOT expose a double prepare. With
14688    // `create_stream(ulid, 64)` against a 200-byte body and a two-round flush,
14689    // production and an edit that visits every stream twice in one pass give
14690    // the same `delta=64 vectored_calls=2 blocks_left=1 bout=73`. The send
14691    // window is SELF-LIMITING: `*parts.window =
14692    // parts.window.saturating_sub(consumed)` zeroes the stream's credit, the
14693    // second prepare computes `min(0, connection_window) == 0`, the converter's
14694    // `self.window > 0` gate returns the chunk untouched, and
14695    // `consumed = 0 - 0 = 0`. A non-incremental prepare always takes every byte
14696    // the window allows, so a second one has nothing left to take.
14697    //
14698    // INVISIBLE TO THOSE OBSERVABLES IS NOT HARMLESS, and the difference is
14699    // the reason this block is worth reading. Two things DO move on that
14700    // second visit, measured on the same fixture:
14701    //
14702    //   - `converter.rs`'s flow-control-stall arm fires. It is the arm reached
14703    //     BECAUSE the window is zero, and its `incr!(names::h2::FLOW_CONTROL_STALL)`
14704    //     is unconditional once there — on `Position::Client` it also fires
14705    //     `incr!(names::backend::FLOW_CONTROL_PAUSED)`. Measured with a `panic!`
14706    //     planted on that `incr!`: the fixture does NOT reach it under
14707    //     production and DOES under the double visit. Those two counters are
14708    //     dashboard signals for a real RFC 9113 §6.9 condition, so a re-entrant
14709    //     write loop would inflate them with stalls that never happened on the
14710    //     wire — an operator would read backpressure into a connection that had
14711    //     none.
14712    //   - `context.debug.push(DebugEvent::S(..))` sits OUTSIDE the prepare
14713    //     gate's closing brace, so it fires once per VISIT, not once per
14714    //     prepare: measured `s_events` 1 -> 2 while `io_events` stays 2. Every
14715    //     extra entry evicts a real one from the bounded `DebugHistory` ring.
14716    //
14717    // So the correct claim is narrow: the BYTES are idempotent, the
14718    // TELEMETRY is not.
14719    //
14720    // And the bytes are idempotent by FOUR independent mechanisms, only one of
14721    // which is the window — a future reader must not conclude the window is
14722    // the guard:
14723    //
14724    //   1. `consumed` debits `*parts.window` AND `flow_control`, which is the
14725    //      self-limiting above;
14726    //   2. `H2Scheduler::note_fired` gates its bucket's round-robin lead on
14727    //      `consumed > 0`, so a zero-consumption re-entry cannot advance the
14728    //      cursor;
14729    //   3. the `rst_sent` `HashSet` in `H2StreamTable` dedups RST_STREAM.
14730    //      This one is load-bearing rather than incidental:
14731    //      `H2BlockConverter::initialize` pushes a RST_STREAM frame on EVERY
14732    //      prepare of an errored kawa with no window gate whatsoever, so
14733    //      without that set a second visit would put a duplicate RST on the
14734    //      wire regardless of the send window;
14735    //   4. `std::mem::take(&mut parts.context.headers_response)` drains the
14736    //      response-header edits, so a re-entry cannot apply them twice.
14737    //
14738    // The RFC 9218 §4 incremental yield is the one path that stops a prepare
14739    // with BOTH window and blocks remaining: `H2BlockConverter`'s DATA arm
14740    // returns `can_continue && !yield_after_data`, and with
14741    // `incremental_mode && incremental_peer_count > 1` it yields after a
14742    // single DATA frame while the window is still wide open. That is why this
14743    // test registers a same-urgency incremental PAIR rather than one stream,
14744    // and why the body must exceed `SETTINGS_MAX_FRAME_SIZE` (16 384) so one
14745    // frame cannot carry it.
14746
14747    /// A response body larger than the default 16 384-byte
14748    /// `SETTINGS_MAX_FRAME_SIZE`, so one DATA frame cannot carry it and the
14749    /// incremental yield leaves the remainder in `blocks`.
14750    static YIELDING_BODY: [u8; 40_000] = [b'x'; 40_000];
14751
14752    /// Payload bytes one yielded prepare converts: the default
14753    /// `SETTINGS_MAX_FRAME_SIZE`, which bounds the frame before the window does.
14754    const YIELDED_PAYLOAD: i32 = 16_384;
14755
14756    /// Bytes one yielded prepare puts in `kawa.out`: the DATA frame header
14757    /// plus its payload.
14758    const YIELDED_FRAME: usize = parser::FRAME_HEADER_SIZE + YIELDED_PAYLOAD as usize;
14759
14760    /// Two registered, same-urgency INCREMENTAL server streams, each carrying
14761    /// `YIELDING_BODY` as one un-prepared chunk and each granted a send window
14762    /// far wider than a single frame.
14763    ///
14764    /// The width is the point: a narrow window would make the prepare stop
14765    /// because it ran out of credit, and a second prepare would then be a
14766    /// no-op whether or not the code is correct. Here the prepare stops
14767    /// because it YIELDED, so credit survives it and a second one would spend
14768    /// more.
14769    fn incremental_pair_fixture(
14770        pool: &Rc<RefCell<Pool>>,
14771        script: &[(usize, SocketResult)],
14772    ) -> (
14773        H2Shell<BackpressuredTlsSocket>,
14774        Context<TestListener>,
14775        [GlobalStreamId; 2],
14776        std::net::TcpStream,
14777    ) {
14778        let (mut connection, peer) = connection_with_backpressure(pool, 0, 0, H2State::Header);
14779        connection.socket.vectored_script = script.iter().copied().collect();
14780        let mut context = test_context(pool);
14781        let mut gids = [0usize; 2];
14782        for (slot, stream_id) in [1u32, 3u32].into_iter().enumerate() {
14783            let gid = context
14784                .create_stream(Ulid::generate(), 1 << 16)
14785                .expect("test context must create a stream");
14786            gids[slot] = gid;
14787            connection
14788                .core
14789                .stream_table
14790                .register(stream_id, gid, connection.core.now);
14791            // Same urgency for both, so they share one bucket and the pass
14792            // census answers `incremental_peer_count == 2`. At 1 the
14793            // converter's solo-bucket guard suppresses the yield and the
14794            // prepare drains to the window, which is the configuration this
14795            // test exists to avoid.
14796            connection.core.scheduler.push_priority(
14797                stream_id,
14798                parser::PriorityPart::Rfc9218 {
14799                    urgency: 3,
14800                    incremental: true,
14801                },
14802            );
14803            let back = &mut context.streams[gid].back;
14804            back.parsing_phase = kawa::ParsingPhase::Body;
14805            back.blocks.push_back(kawa::Block::Chunk(kawa::Chunk {
14806                data: kawa::Store::Static(&YIELDING_BODY),
14807            }));
14808        }
14809        (connection, context, gids, peer)
14810    }
14811
14812    /// One write pass debits the connection send window once per stream, even
14813    /// when that stream's flush goes round twice.
14814    ///
14815    /// Stream 1's flush is scripted to take two rounds — `(20, WouldBlock)`
14816    /// then the rest — so the pass contains exactly the round-again shape the
14817    /// flush tests above pin, and this test says the prepare does NOT come
14818    /// along for the second round. Stream 3's single round is the control:
14819    /// both streams yield after one frame, and both must cost the same credit.
14820    ///
14821    /// WHAT THE EXISTING FIXTURE CANNOT SEE. `a_partial_write_reporting_would_block_continues_the_pass`
14822    /// drives the identical two-round flush and stays green under every edit
14823    /// below: its stream's parsing phase leaves the prepare gate shut by
14824    /// construction, so it never runs a prepare at all and no amount of
14825    /// re-preparing is representable in it. Its through-`writable()` twin
14826    /// cannot see it either, for the same reason.
14827    ///
14828    /// TO SEE THIS RED: in `begin_scheduler_pass`, insert
14829    /// `let order: Vec<StreamId> = order.iter().chain(order.iter()).copied().collect();`
14830    /// immediately before `pass.adopt_scheduler_pass(converter_pass, order, census);`,
14831    /// so each stream takes a second turn — prepare included — within one pass.
14832    /// Measured: this test fails on its own first assertion, `one write pass
14833    /// must debit the connection send window once per stream`, with
14834    /// `left: 65535, right: 32768`, and it is the ONLY failure in the suite.
14835    /// The four frames the mutated pass emits instead
14836    /// of two also move `vectored_calls` from 3 to 5 and each stream's `bout`
14837    /// from 16 393 to 32 786 / 32 785, so the window is not a privileged
14838    /// witness here — but it is the only one that names the SPENT CREDIT, and
14839    /// a window debited for bytes the wire never carried is the failure that
14840    /// stalls a connection. The same assertion is also the third failure under
14841    /// the pass-terminator edit documented on
14842    /// `a_partial_write_reporting_would_block_continues_the_pass_through_writable`,
14843    /// where it reads `left: 16384, right: 32768` because stream 1's stall
14844    /// ends the pass before stream 3 is prepared.
14845    ///
14846    /// This is NOT keyed on the partial write, and the name says so. The
14847    /// inversion makes the rounds visible — `H2WritePhase::Flush` is re-entered
14848    /// once per round — but the prepare is not in that phase to be repeated:
14849    /// it lives in `H2WritePhase::Prepare`, which a round-again never returns
14850    /// to. Forcing a second visit of the whole order is the smallest honest
14851    /// statement of the same bug, and it is what a restructure that folded the
14852    /// two phases together would actually produce.
14853    #[test]
14854    fn a_yielded_incremental_stream_is_prepared_once_per_pass() {
14855        let pool = make_pool_for_invariant_16();
14856        let (mut connection, mut context, gids, _peer) = incremental_pair_fixture(
14857            &pool,
14858            &[
14859                // Stream 1: a partial write that reports WouldBlock, then the
14860                // remainder. Stream 3: one round.
14861                (20, SocketResult::WouldBlock),
14862                (99_999, SocketResult::Continue),
14863                (99_999, SocketResult::Continue),
14864            ],
14865        );
14866        let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
14867
14868        let window_before = connection.core.flow_control.window();
14869        assert_eq!(
14870            window_before, DEFAULT_INITIAL_WINDOW_SIZE as i32,
14871            "premise: the connection send window starts at the RFC 9113 \
14872             default, so the debit below is the pass's doing"
14873        );
14874        for gid in gids {
14875            assert!(
14876                context.streams[gid].back.is_main_phase(),
14877                "premise: the prepare gate must be OPEN for both streams, or \
14878                 no window is spent at all"
14879            );
14880        }
14881
14882        let result = connection.writable(&mut context, EndpointClient(&mut router));
14883
14884        assert_eq!(
14885            window_before - connection.core.flow_control.window(),
14886            2 * YIELDED_PAYLOAD,
14887            "one write pass must debit the connection send window once per \
14888             stream: a second prepare would spend credit for bytes this pass \
14889             never put on the wire"
14890        );
14891        assert_eq!(
14892            connection.socket.vectored_calls, 3,
14893            "the pass must be two flush rounds for stream 1 and one for \
14894             stream 3 — without the two-round flush this test would not be \
14895             asserting anything about re-entry"
14896        );
14897        for gid in gids {
14898            assert_eq!(
14899                context.streams[gid].metrics.bout, YIELDED_FRAME,
14900                "each yielded prepare puts exactly one DATA frame on the wire"
14901            );
14902            assert!(
14903                !context.streams[gid].back.blocks.is_empty(),
14904                "premise: the yield must leave the rest of the body queued — \
14905                 with empty blocks a second prepare could not spend anything \
14906                 and this test could not fail"
14907            );
14908        }
14909        assert!(
14910            matches!(result, MuxResult::Continue),
14911            "a yielded write pass continues, got {result:?}"
14912        );
14913    }
14914
14915    /// The send window granted to the stream that makes this pass's progress.
14916    /// Narrower than `YIELDING_BODY`, so its prepare converts
14917    /// `PARTIAL_STREAM_WINDOW` payload bytes and leaves the rest in `blocks`.
14918    const PARTIAL_STREAM_WINDOW: u32 = 64;
14919
14920    /// Bytes that stream reaches the socket with: the DATA frame header plus
14921    /// its window-capped payload.
14922    const PARTIAL_FRAME: usize = parser::FRAME_HEADER_SIZE + PARTIAL_STREAM_WINDOW as usize;
14923
14924    /// A pass's byte total accumulates across streams: a later stream that
14925    /// writes nothing must not erase what an earlier one delivered.
14926    ///
14927    /// `pass.total_bytes_written.saturating_add(pass.stream_bytes)` in
14928    /// `H2WritePhase::Flush` is the whole of it, and nothing pinned it. `finalize_write` hands the
14929    /// total to `h2_close::finalize_action` as `made_progress`, and
14930    /// `made_progress && any_pending_back()` is LIFECYCLE §9 invariant 16 — the
14931    /// arm that RETAINS `Ready::WRITABLE` for a pass that moved bytes and left
14932    /// blocks behind. Drop the accumulation and a two-stream pass reports the
14933    /// LAST stream's count: stream 1 delivers a frame and keeps its remainder
14934    /// queued, stream 3 is eligible but window-blocked and writes nothing, the
14935    /// total reads 0, `expect_write` is parked on neither so `Parked` cannot
14936    /// mask it, `RetainPendingBack` is skipped and the pass falls through to
14937    /// `Quiesce`. `Quiesce` strips `Ready::WRITABLE`; edge-triggered epoll
14938    /// never re-fires; stream 1's body is stranded in `blocks` with nothing
14939    /// scheduled to drain it. That is a hung response, not a lost metric.
14940    ///
14941    /// The two streams differ only in their send window, which is what puts
14942    /// one on each side of the accumulation: 64 bytes buys stream 1 one
14943    /// window-capped DATA frame, and 0 buys stream 3 the converter's
14944    /// flow-control-stall arm, which returns the chunk untouched and leaves
14945    /// `kawa.out` empty so its flush never calls the socket at all.
14946    ///
14947    /// TO SEE THIS RED: in `poll_write_target`'s `H2WritePhase::Flush` arm,
14948    /// change the accumulation
14949    /// `pass.total_bytes_written = pass.total_bytes_written.saturating_add(pass.stream_bytes);`
14950    /// into `pass.total_bytes_written = pass.stream_bytes;` — the shape a
14951    /// rewrite that folds the resume and main flush paths into one per-stream
14952    /// counter naturally reaches for. Measured: this test fails on its own first
14953    /// assertion, `a pass that delivered bytes and left blocks queued must
14954    /// RETAIN WRITABLE interest`, with `left: Error | Hup, right: Writable |
14955    /// Error | Hup`, and it is the only failure in the suite. Measured without
14956    /// this test in the tree, that same edit leaves every other test green —
14957    /// which is what makes this one worth its lines.
14958    #[test]
14959    fn a_later_zero_byte_stream_does_not_erase_the_pass_progress() {
14960        let pool = make_pool_for_invariant_16();
14961        let (mut connection, _peer) = connection_with_backpressure(&pool, 0, 0, H2State::Header);
14962        // One entry: stream 1's frame. Stream 3 puts nothing in `kawa.out`, so
14963        // its flush loop never runs and never reaches the socket.
14964        connection.socket.vectored_script =
14965            [(99_999, SocketResult::Continue)].into_iter().collect();
14966        let mut context = test_context(&pool);
14967        let mut gids = [0usize; 2];
14968        for (slot, (stream_id, window)) in [(1u32, PARTIAL_STREAM_WINDOW), (3u32, 0)]
14969            .into_iter()
14970            .enumerate()
14971        {
14972            let gid = context
14973                .create_stream(Ulid::generate(), window)
14974                .expect("test context must create a stream");
14975            gids[slot] = gid;
14976            connection
14977                .core
14978                .stream_table
14979                .register(stream_id, gid, connection.core.now);
14980            let back = &mut context.streams[gid].back;
14981            back.parsing_phase = kawa::ParsingPhase::Body;
14982            back.blocks.push_back(kawa::Block::Chunk(kawa::Chunk {
14983                data: kawa::Store::Static(&YIELDING_BODY),
14984            }));
14985        }
14986        let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
14987
14988        assert!(
14989            connection.core.readiness.interest.is_writable(),
14990            "premise: WRITABLE interest starts armed, so retaining it is not \
14991             vacuous, got {:?}",
14992            connection.core.readiness
14993        );
14994
14995        let result = connection.writable(&mut context, EndpointClient(&mut router));
14996
14997        assert_eq!(
14998            connection.core.readiness.interest,
14999            Ready::WRITABLE | Ready::HUP | Ready::ERROR,
15000            "a pass that delivered bytes and left blocks queued must RETAIN \
15001             WRITABLE interest (LIFECYCLE §9 invariant 16): stripping it \
15002             strands the remainder, because edge-triggered epoll will not \
15003             re-fire on its own"
15004        );
15005        assert_eq!(
15006            context.streams[gids[0]].metrics.bout, PARTIAL_FRAME,
15007            "premise: the FIRST stream really did deliver a frame — the \
15008             accumulation has nothing to preserve otherwise"
15009        );
15010        assert_eq!(
15011            context.streams[gids[1]].metrics.bout, 0,
15012            "premise: the SECOND stream really did write nothing, so it is the \
15013             stream whose count would overwrite the total"
15014        );
15015        assert_eq!(
15016            connection.socket.vectored_calls, 1,
15017            "only the first stream reaches the socket: a zero window leaves \
15018             `kawa.out` empty and its flush loop never runs"
15019        );
15020        for gid in gids {
15021            assert!(
15022                !context.streams[gid].back.blocks.is_empty(),
15023                "premise: both streams must leave blocks queued, or \
15024                 any_pending_back answers false and RetainPendingBack is \
15025                 unreachable whatever the byte total says"
15026            );
15027        }
15028        assert_eq!(
15029            connection.core.stream_table.expect_write(),
15030            None,
15031            "premise: nothing is parked, so `Parked` cannot decide this pass \
15032             ahead of the byte total"
15033        );
15034        assert!(
15035            matches!(result, MuxResult::Continue),
15036            "a retained write pass continues, got {result:?}"
15037        );
15038    }
15039
15040    // ── force_disconnect's server arm with records pending ──────────────
15041    //
15042    // `h2_close::force_disconnect_action` is exhaustively tabled over its two
15043    // booleans in that module. Nothing called `ConnectionH2::force_disconnect`
15044    // itself, so the CALLER half — the `H2Shell::tls_wants_write` query
15045    // that feeds the table and the wiring of `ReArmAndContinue` to interest,
15046    // flush and return value — had no coverage. #1454 asks for this site by
15047    // name.
15048    //
15049    // Shape targeted: a SINGLE query, not a triple. `force_disconnect` reads
15050    // the answer once into `tls_wants_write`, above its `match` and therefore
15051    // for both position arms, feeds the table and reports the same value from
15052    // every debug line; there is no `H2Shell::flush_tls_records` here and
15053    // so no second question to reconstruct.
15054
15055    /// A server connection whose TLS records are still buffered re-arms
15056    /// instead of closing the session.
15057    ///
15058    /// `MuxResult::CloseSession` triggers `shutdown(Write)`, which sends FIN
15059    /// and destroys whatever rustls is still holding — the client reads a
15060    /// truncated response. So the live-peer, records-pending row of
15061    /// `force_disconnect_action` must keep WRITABLE and let the writable path
15062    /// flush.
15063    ///
15064    /// TO SEE THIS RED: in `force_disconnect`'s `Position::Server` arm, pass
15065    /// `false` in place of `tls_wants_write` to `force_disconnect_action`.
15066    /// Measured: this test fails on its own first assertion, `a server still
15067    /// holding TLS records must not close: CloseSession sends FIN and destroys
15068    /// them, got CloseSession`, while `h2_close`'s own
15069    /// `force_disconnect_arm_is_exhaustive` stays green — the table is not the
15070    /// caller, which is why this test exists. Passing `true` instead would NOT
15071    /// redden this test; catching that constant ON THIS DIRECT PATH is what
15072    /// the sibling below is for — it is not the suite's only witness against
15073    /// `true`, and its own doc says which other test is.
15074    ///
15075    /// The interest assertion has its own recipe: delete
15076    /// `self.readiness.interest = Ready::WRITABLE | Ready::HUP | Ready::ERROR;`
15077    /// from the same arm. Measured: `the re-arm must restore WRITABLE interest
15078    /// so the writable path is called again to flush`, `left: Hup, right:
15079    /// Writable | Error | Hup`. That recipe only bites because the test
15080    /// withdraws the bit first — asserting it against the fixture's own
15081    /// constructor argument passed under this very mutation.
15082    #[test]
15083    fn force_disconnect_re_arms_while_tls_records_are_pending() {
15084        let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
15085        // A kernel that accepts nothing, so the records outlive any flush.
15086        let (mut connection, _peer) = connection_with_backpressure(&pool, 2, 0, H2State::GoAway);
15087
15088        assert!(
15089            connection.socket.socket_wants_write(),
15090            "premise: this harness must report buffered TLS records"
15091        );
15092        assert!(
15093            !connection.core.peer_gone_after_final_goaway(),
15094            "premise: the peer must still be live, or the table short-circuits \
15095             to CloseSession on the first boolean and the record count is never \
15096             read"
15097        );
15098        // Withdraw WRITABLE interest first. `connection_with_backpressure`
15099        // hands `WRITABLE | HUP | ERROR` to `ConnectionH2::new` as the
15100        // connection's INTEREST, so asserting on it untouched would be reading
15101        // this fixture's own argument back and would hold even if the re-arm
15102        // arm never assigned it. Starting from `HUP` alone makes the
15103        // assignment the only thing that can restore the bit.
15104        connection.core.readiness.interest = Ready::HUP;
15105        assert!(
15106            !connection.core.readiness.event.is_writable(),
15107            "premise: the WRITABLE event must start clear, so the signal below \
15108             is the only thing that can set it, got {:?}",
15109            connection.core.readiness
15110        );
15111
15112        let result = connection.force_disconnect();
15113
15114        assert!(
15115            matches!(result, MuxResult::Continue),
15116            "a server still holding TLS records must not close: CloseSession \
15117             sends FIN and destroys them, got {result:?}"
15118        );
15119        assert_eq!(
15120            connection.core.readiness.interest,
15121            Ready::WRITABLE | Ready::HUP | Ready::ERROR,
15122            "the re-arm must restore WRITABLE interest so the writable path is \
15123             called again to flush, got {:?}",
15124            connection.core.readiness
15125        );
15126        assert!(
15127            connection.core.readiness.event.is_writable(),
15128            "the re-arm must signal the edge-triggered WRITABLE event, or the \
15129             event loop never revisits this connection, got {:?}",
15130            connection.core.readiness
15131        );
15132    }
15133
15134    /// The same call with nothing buffered closes the session.
15135    ///
15136    /// The other row of the table. It does NOT rest on being the only thing
15137    /// that catches a hard-wired `true`: the suite already caught that
15138    /// constant without it. Measured on that mutation, TWO tests fail — this
15139    /// test AND
15140    /// `a_flush_that_succeeds_closes_within_one_writable_call` above, which
15141    /// predates it and reaches `force_disconnect` through `writable()`'s
15142    /// GoAway fall-through, failing with `a drained flush must reach the
15143    /// disconnect in the SAME writable call, not defer it to another tick:
15144    /// got Continue`.
15145    ///
15146    /// What this test adds is the PATH, not the count: it calls
15147    /// `ConnectionH2::force_disconnect` directly, so a red here names the
15148    /// query under test, where the fall-through route arrives through a write
15149    /// pass and a state machine that can each shift for reasons of their own.
15150    ///
15151    /// TO SEE THIS RED: pass `true` in place of `tls_wants_write` to
15152    /// `force_disconnect_action` in `force_disconnect`'s `Position::Server`
15153    /// arm. Measured: this test fails with `a server holding nothing must
15154    /// close rather than wait for a flush that has nothing to flush, got
15155    /// Continue`, while the sibling above stays green. The mirror constant
15156    /// `false` is measured at exactly ONE failure, and that failure is
15157    /// the sibling above — so neither constant can replace the query, and the
15158    /// sibling is the suite's only witness against `false`.
15159    #[test]
15160    fn force_disconnect_closes_when_no_tls_records_are_pending() {
15161        let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
15162        // Same fixture, zero records: the query answers `false`.
15163        let (mut connection, _peer) = connection_with_backpressure(&pool, 0, 0, H2State::GoAway);
15164
15165        assert!(
15166            !connection.socket.socket_wants_write(),
15167            "premise: nothing buffered, so the table reads its second boolean \
15168             as false"
15169        );
15170        assert!(
15171            !connection.core.peer_gone_after_final_goaway(),
15172            "premise: the peer must still be live, so the close comes from the \
15173             record count and not from the first boolean"
15174        );
15175
15176        let result = connection.force_disconnect();
15177
15178        assert!(
15179            matches!(result, MuxResult::CloseSession),
15180            "a server holding nothing must close rather than wait for a flush \
15181             that has nothing to flush, got {result:?}"
15182        );
15183    }
15184
15185    /// A force-disconnect raised from the READ path still re-arms, because the
15186    /// shell settles it before the pass returns.
15187    ///
15188    /// The sibling above calls `H2Shell::force_disconnect` directly, where the
15189    /// raise and the settlement are two statements of one function. This one
15190    /// enters through `H2Shell::readable`, which is the shape that made the
15191    /// inversion necessary: `ConnectionH2::handle_read` force-disconnects at
15192    /// seven sites, its `pub` signature returns `MuxResult` and cannot carry an
15193    /// `H2ForceDisconnectTarget`, so the core parks one and answers
15194    /// provisionally. Nothing the core does afterwards writes `readiness`; only
15195    /// `H2Shell::settled` does, and only if the shell remembers to call it.
15196    ///
15197    /// `Mux` reads `Readiness::filter_interest` between entry points to decide
15198    /// whether a connection gets a `writable` at all, so a settlement deferred
15199    /// to the next entry point is not the same as one performed here: the
15200    /// connection would be left with records in rustls and no WRITABLE
15201    /// interest to come back on.
15202    ///
15203    /// TO SEE THIS RED: in `H2Shell::readable`, return `result` instead of
15204    /// `self.settled(result)`. Measured: this test fails on its interest
15205    /// assertion, `a force-disconnect raised inside handle_read must still
15206    /// re-arm WRITABLE interest before the read pass returns`, `left: Hup`,
15207    /// `right: Writable | Error | Hup`, while
15208    /// `force_disconnect_re_arms_while_tls_records_are_pending` stays green —
15209    /// the direct call is not the read path, which is why both exist.
15210    #[test]
15211    fn a_force_disconnect_raised_inside_handle_read_is_settled_before_the_pass_returns() {
15212        use std::io::Write;
15213
15214        let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
15215        // A kernel that accepts nothing, so the records outlive any flush and
15216        // `force_disconnect_action` takes its `ReArmAndContinue` row.
15217        let (mut connection, mut peer) =
15218            connection_with_backpressure(&pool, 2, 0, H2State::ClientPreface);
15219        let mut context = test_context(&pool);
15220        let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
15221
15222        assert!(
15223            connection.socket.socket_wants_write(),
15224            "premise: this harness must report buffered TLS records, or the \
15225             settlement takes the closing row and writes no interest at all"
15226        );
15227        assert!(
15228            !connection.core.peer_gone_after_final_goaway(),
15229            "premise: the peer must still be live, or the decision \
15230             short-circuits to CloseSession on its first boolean and never \
15231             reads the record count"
15232        );
15233
15234        // A preface whose very first octet is wrong: `handle_read`'s early
15235        // guard refuses it and reaches `force_disconnect` without the
15236        // connection ever leaving `H2State::ClientPreface` on its own.
15237        let mut wire = Vec::with_capacity(CLIENT_PREFACE_SIZE);
15238        wire.extend_from_slice(b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n");
15239        wire.extend_from_slice(&[0, 0, 0, 4, 0, 0, 0, 0, 0]);
15240        wire[0] ^= 0xff;
15241        peer.write_all(&wire).expect("loopback write must complete");
15242        peer.flush().expect("loopback flush must complete");
15243
15244        // Withdraw WRITABLE interest first. The fixture hands
15245        // `WRITABLE | HUP | ERROR` to the constructor, so asserting on it
15246        // untouched would read the fixture's own argument back and would hold
15247        // even if nothing settled. Starting from `HUP` alone makes the
15248        // settlement the only thing that can restore the bit.
15249        connection.core.readiness.interest = Ready::HUP;
15250        assert!(
15251            !connection.core.readiness.event.is_writable(),
15252            "premise: the WRITABLE event must start clear, so the signal below \
15253             is the only thing that can set it, got {:?}",
15254            connection.core.readiness
15255        );
15256
15257        let mut result = MuxResult::Continue;
15258        for _ in 0..64 {
15259            result = connection.readable(&mut context, EndpointClient(&mut router));
15260            if matches!(connection.core.state, H2State::Error) {
15261                break;
15262            }
15263            std::thread::yield_now();
15264        }
15265
15266        assert!(
15267            matches!(connection.core.state, H2State::Error),
15268            "premise: the read pass must have reached force_disconnect, which \
15269             assigns H2State::Error, got {:?}",
15270            connection.core.state
15271        );
15272        assert!(
15273            matches!(result, MuxResult::Continue),
15274            "a server still holding TLS records must not close: CloseSession \
15275             sends FIN and destroys them, got {result:?}"
15276        );
15277        assert_eq!(
15278            connection.core.readiness.interest,
15279            Ready::WRITABLE | Ready::HUP | Ready::ERROR,
15280            "a force-disconnect raised inside handle_read must still re-arm \
15281             WRITABLE interest before the read pass returns, got {:?}",
15282            connection.core.readiness
15283        );
15284        assert!(
15285            connection.core.readiness.event.is_writable(),
15286            "the settlement must signal the edge-triggered WRITABLE event, or \
15287             the event loop never revisits this connection, got {:?}",
15288            connection.core.readiness
15289        );
15290        assert!(
15291            connection.core.poll_force_disconnect().is_none(),
15292            "the settlement must have TAKEN the parked target, so a second \
15293             settlement cannot run"
15294        );
15295    }
15296
15297    /// Build a bare server-side `ConnectionH2` for tests that only exercise
15298    /// connection-level bookkeeping. The socket is never read or written.
15299    ///
15300    /// Returns the accepted peer alongside it; keep it alive for the duration
15301    /// of the test so the socket stays connected and reads answer `WouldBlock`
15302    /// instead of `ECONNREFUSED`.
15303    fn test_h2_connection(
15304        pool: &Rc<RefCell<Pool>>,
15305        graceful_shutdown_deadline: Option<Duration>,
15306    ) -> (H2Shell<mio::net::TcpStream>, std::net::TcpStream) {
15307        let (socket, peer) = connected_socket();
15308        let connection = H2Shell::new(
15309            Ulid::generate(),
15310            socket,
15311            Position::Server,
15312            &mut PoolBufferSource::new(Rc::downgrade(pool)),
15313            H2FloodConfig::default(),
15314            H2ConnectionConfig::default(),
15315            Duration::from_secs(30),
15316            graceful_shutdown_deadline,
15317            Duration::from_secs(30),
15318            Some((H2StreamId::Zero, CLIENT_PREFACE_SIZE)),
15319            Ready::READABLE | Ready::HUP | Ready::ERROR,
15320        )
15321        .expect("a pool with free buffers must yield an H2 connection");
15322        (connection, peer)
15323    }
15324
15325    /// `snapshot_rtts` samples the frontend once per connection, when the
15326    /// first stream of the connection is logged, and hands every later stream
15327    /// the same value, in the same pass or in a later one (issues #1339 Q11
15328    /// and #1590).
15329    ///
15330    /// The first two calls stand for two streams recycling inside one pass:
15331    /// all five production callers of this method sit on a recycle or reset
15332    /// path, and more than one of them can run in a single `write_streams`
15333    /// sweep. The third, through a fresh `ShellEndpoint`, stands for a stream
15334    /// logged in a later pass. The counter is
15335    /// [`super::super::tcp_info_reads`], bumped by every
15336    /// `getsockopt(TCP_INFO)` the mux issues.
15337    ///
15338    /// The premise is that the sample is `Some`: the connection sits on a live
15339    /// loopback socket, so `TCP_INFO` answers. Without it a `local_rtt` that
15340    /// had lost the socket would pass the equality assertions while reporting
15341    /// nothing at all.
15342    ///
15343    /// TO SEE THIS RED: in `memoized_rtt` (`lib/src/protocol/mux/mod.rs`),
15344    /// delete the early `return sample;` of the memo hit. Every call then
15345    /// reads the socket and the second assertion fails with ``a second stream
15346    /// finishing in the SAME pass must reuse the connection sample, not read
15347    /// TCP_INFO again``, `left: 2`, `right: 1`.
15348    #[test]
15349    fn snapshot_rtts_samples_the_frontend_once_per_connection() {
15350        let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
15351        let (connection, _peer) = test_h2_connection(&pool, None);
15352        let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
15353        let mut inner = EndpointClient(&mut router);
15354
15355        let before = super::super::tcp_info_reads();
15356        let endpoint = ShellEndpoint::new(&mut inner, &connection.socket, &connection.local_rtt);
15357        let first = connection
15358            .core
15359            .snapshot_rtts(&endpoint, Some(mio::Token(1)));
15360        assert!(
15361            first.0.is_some(),
15362            "premise: TCP_INFO must answer on a live loopback frontend"
15363        );
15364        let second = connection
15365            .core
15366            .snapshot_rtts(&endpoint, Some(mio::Token(2)));
15367        assert_eq!(
15368            super::super::tcp_info_reads() - before,
15369            1,
15370            "a second stream finishing in the SAME pass must reuse the \
15371             connection sample, not read TCP_INFO again"
15372        );
15373        assert_eq!(
15374            second.0, first.0,
15375            "every stream finishing in one pass reports one client_rtt — the \
15376             identity #1339 Q11 traded the per-recycle syscall for"
15377        );
15378        assert_eq!(
15379            (first.1, second.1),
15380            (None, None),
15381            "the router holds no backend, so server_rtt stays None and costs \
15382             no read"
15383        );
15384
15385        let endpoint = ShellEndpoint::new(&mut inner, &connection.socket, &connection.local_rtt);
15386        let next_pass = connection.core.snapshot_rtts(&endpoint, None);
15387        assert_eq!(
15388            super::super::tcp_info_reads() - before,
15389            1,
15390            "a stream logged in a later pass must reuse the connection's one \
15391             sample, not read TCP_INFO again"
15392        );
15393        assert_eq!(
15394            next_pass.0, first.0,
15395            "every stream of one connection reports that connection's one \
15396             client_rtt"
15397        );
15398    }
15399
15400    /// A pass that logs no stream reads no `TCP_INFO`, and a backend H2
15401    /// connection never reads one for the access log (issue #1590).
15402    ///
15403    /// TO SEE THIS RED: make `ShellEndpoint::new` sample eagerly — call
15404    /// `super::sample_rtt(socket.socket_ref())` and store it in `sample` —
15405    /// which is the shape `Mux::refresh_client_rtt` had. The first assertion
15406    /// fails with ``a pass that logs no stream must not read TCP_INFO``,
15407    /// `left: 1`, `right: 0`.
15408    #[test]
15409    fn a_pass_that_logs_nothing_reads_no_tcp_info() {
15410        let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
15411        let (mut connection, _peer) = test_h2_connection(&pool, None);
15412        let mut context = test_context(&pool);
15413        let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
15414
15415        let before = super::super::tcp_info_reads();
15416        connection.cancel_timed_out_streams(&mut context, &mut EndpointClient(&mut router));
15417        assert_eq!(
15418            super::super::tcp_info_reads() - before,
15419            0,
15420            "a pass that logs no stream must not read TCP_INFO"
15421        );
15422        assert_eq!(connection.local_rtt.get(), None);
15423
15424        connection.core.position = Position::Client(
15425            "rtt-cluster".into(),
15426            super::super::BackendId {
15427                slot: super::super::BackendSlot(0),
15428                backend_id: Rc::from("rtt-backend"),
15429                address: "127.0.0.1:1".parse().expect("a literal socket address"),
15430                metric_incarnation: Default::default(),
15431            },
15432            BackendStatus::Connected,
15433        );
15434        let mut inner = EndpointClient(&mut router);
15435        let endpoint = ShellEndpoint::new(&mut inner, &connection.socket, &connection.local_rtt);
15436        assert_eq!(
15437            connection
15438                .core
15439                .snapshot_rtts(&endpoint, Some(mio::Token(1))),
15440            (None, None),
15441            "a backend H2 connection must not report RTTs onto the shared stream"
15442        );
15443        assert_eq!(
15444            super::super::tcp_info_reads() - before,
15445            0,
15446            "a backend H2 connection must not read TCP_INFO for the access log"
15447        );
15448    }
15449
15450    /// Two streams reaped in one pass: two access-log lines, one frontend
15451    /// `TCP_INFO` read between them, and the same `client_rtt` on both lines.
15452    ///
15453    /// Drives the real log path — `H2Shell::cancel_timed_out_streams` through
15454    /// `ShellEndpoint`, `snapshot_rtts` and `Stream::generate_access_log` —
15455    /// and reads the lines the logger wrote, so it pins what an operator sees:
15456    /// the `client_rtt` cell is present (not `-`) and coherent across the
15457    /// pass, and `server_rtt` is `-` for streams that never reached a backend.
15458    ///
15459    /// TO SEE THIS RED: in `ConnectionH2::snapshot_rtts`, return `None` in
15460    /// place of `endpoint.local_rtt()`. Both lines then carry `-` in the
15461    /// `client_rtt` cell and the presence assertion fails.
15462    #[test]
15463    fn streams_reaped_in_one_pass_log_one_present_client_rtt() {
15464        let reads = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
15465        let reads_in_pass = reads.clone();
15466        let output = crate::capture_test_logs(move || {
15467            let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 8, 16384)));
15468            let (mut connection, _peer) = test_h2_connection(&pool, None);
15469            let mut context = test_context(&pool);
15470            let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
15471
15472            let armed_at = connection.core.now;
15473            for stream_id in [1, 3] {
15474                let gid = context
15475                    .create_stream(Ulid::generate(), 1 << 16)
15476                    .expect("test context must create a stream");
15477                connection
15478                    .core
15479                    .stream_table
15480                    .register(stream_id, gid, armed_at);
15481            }
15482            context.now = armed_at + connection.core.stream_idle_timeout + Duration::from_millis(1);
15483
15484            let before = super::super::tcp_info_reads();
15485            connection.cancel_timed_out_streams(&mut context, &mut EndpointClient(&mut router));
15486            reads_in_pass.store(
15487                super::super::tcp_info_reads() - before,
15488                std::sync::atomic::Ordering::Relaxed,
15489            );
15490        });
15491
15492        let cells: Vec<(&str, &str)> = output
15493            .lines()
15494            .filter_map(|line| {
15495                line.split_whitespace().find_map(|token| {
15496                    let cells: Vec<&str> = token.split('/').collect();
15497                    (cells.len() == 5).then(|| (cells[3], cells[4]))
15498                })
15499            })
15500            .collect();
15501        assert_eq!(
15502            cells.len(),
15503            2,
15504            "two reaped streams must write two access-log lines, got: {output}"
15505        );
15506        for (client_rtt, server_rtt) in &cells {
15507            assert_ne!(
15508                *client_rtt, "-",
15509                "the access log's client_rtt must be present, got: {output}"
15510            );
15511            assert_eq!(
15512                *server_rtt, "-",
15513                "a stream that never reached a backend has no server_rtt, got: {output}"
15514            );
15515        }
15516        assert_eq!(
15517            cells[0].0, cells[1].0,
15518            "streams logged in one pass share one client_rtt, got: {output}"
15519        );
15520        assert_eq!(
15521            reads.load(std::sync::atomic::Ordering::Relaxed),
15522            1,
15523            "two streams logged in one pass must cost one frontend TCP_INFO \
15524             read and no backend read"
15525        );
15526    }
15527
15528    /// The graceful-shutdown budget is armed from the `now` handed to
15529    /// `graceful_goaway` and evaluated against `ConnectionH2::now` — not
15530    /// against the real clock at either end.
15531    ///
15532    /// `armed_at` is deliberately 100 s in the future relative to the
15533    /// connection's construction snapshot. That gap is what makes the test
15534    /// discriminating: a `graceful_goaway` that armed `started_at` from
15535    /// `Instant::now()` would put it ~100 s in the PAST relative to
15536    /// `armed_at`, and the "not yet elapsed" assertion would trip.
15537    ///
15538    /// To SEE THIS RED: restore either half of the old behaviour.
15539    /// (a) `self.started_at = Some(Instant::now());` in
15540    ///     `h2_drain::H2DrainState::begin_graceful_drain` — the `assert_eq!`
15541    ///     on `drain.__test_started_at()` fails: ``assertion `left == right`
15542    ///     failed: graceful_goaway must arm the budget from its `now`
15543    ///     parameter / left: Some(Instant { tv_sec: 107343, .. }) / right:
15544    ///     Some(Instant { tv_sec: 107443, .. })`` — the two differing by
15545    ///     exactly the 100 s offset above.
15546    /// (b) `started_at.elapsed() >= deadline` in
15547    ///     `h2_drain::H2DrainState::deadline_elapsed` — the LAST assertion
15548    ///     fails with `advancing only the connection snapshot past the
15549    ///     budget must force close`, because no real time passes in this
15550    ///     test, so the forced-close budget never expires however far the
15551    ///     connection's clock is moved.
15552    #[test]
15553    fn graceful_shutdown_deadline_is_evaluated_against_the_connection_snapshot() {
15554        let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
15555        let deadline = Duration::from_secs(5);
15556        let (mut connection, _peer) = test_h2_connection(&pool, Some(deadline));
15557
15558        // Nothing armed yet: `started_at` is None, so the budget cannot expire.
15559        assert!(
15560            !connection.core.graceful_shutdown_deadline_elapsed(),
15561            "an unarmed drain must never report its budget as elapsed"
15562        );
15563
15564        let armed_at = connection.core.now + Duration::from_secs(100);
15565        connection.graceful_goaway(armed_at);
15566        assert_eq!(
15567            connection.core.drain.__test_started_at(),
15568            Some(armed_at),
15569            "graceful_goaway must arm the budget from its `now` parameter"
15570        );
15571
15572        // One millisecond short of the budget, measured on the connection's
15573        // clock. Real elapsed time here is microseconds.
15574        connection.core.now = armed_at + deadline - Duration::from_millis(1);
15575        assert!(
15576            !connection.core.graceful_shutdown_deadline_elapsed(),
15577            "the budget must not expire before `started_at + deadline` on the \
15578             connection's own clock"
15579        );
15580
15581        connection.core.now = armed_at + deadline;
15582        assert!(
15583            connection.core.graceful_shutdown_deadline_elapsed(),
15584            "advancing only the connection snapshot past the budget must force close"
15585        );
15586    }
15587
15588    /// The RFC 9113 §5.1.2 back-pressure window is driven by
15589    /// `ConnectionH2::now`, so a refusal burst is weighed against the window
15590    /// that was open when the pass started — it cannot decay mid-pass.
15591    ///
15592    /// To SEE THIS RED: restore
15593    /// `if self.refuse_window_start.elapsed() >= BACKPRESSURE_WINDOW_DURATION`
15594    /// in `record_refusal_for_backpressure`. The window then never rolls over
15595    /// (no real time passes) and the 50th refusal lands in the same window as
15596    /// the first 49, so the `refuse_count_window` assertion fails first:
15597    /// ``assertion `left == right` failed: a refusal in a new window must
15598    /// restart the count, not top up the old one / left: 1000 / right: 1``. The
15599    /// two assertions after it would also fail — `mcs_backpressure_applied`
15600    /// becomes true and `settings_max_concurrent_streams` is halved off a burst
15601    /// that actually spanned two windows.
15602    #[test]
15603    fn backpressure_window_rolls_over_on_the_connection_snapshot() {
15604        let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
15605        let (mut connection, _peer) = test_h2_connection(&pool, None);
15606        let advertised = connection
15607            .core
15608            .local_settings
15609            .settings_max_concurrent_streams;
15610
15611        // One short of the burst threshold, all inside the first window.
15612        for _ in 0..BACKPRESSURE_REFUSAL_THRESHOLD - 1 {
15613            connection.core.record_refusal_for_backpressure();
15614        }
15615        assert_eq!(
15616            connection.core.refuse_count_window,
15617            BACKPRESSURE_REFUSAL_THRESHOLD - 1
15618        );
15619        assert!(
15620            !connection.core.mcs_backpressure_applied,
15621            "back-pressure must not apply below the refusal threshold"
15622        );
15623
15624        // Cross into the next window on the connection's clock alone.
15625        connection.core.now += BACKPRESSURE_WINDOW_DURATION;
15626        connection.core.record_refusal_for_backpressure();
15627
15628        assert_eq!(
15629            connection.core.refuse_count_window, 1,
15630            "a refusal in a new window must restart the count, not top up the old one"
15631        );
15632        assert!(
15633            !connection.core.mcs_backpressure_applied,
15634            "two refusals spread across two windows are not a burst"
15635        );
15636        assert_eq!(
15637            connection
15638                .core
15639                .local_settings
15640                .settings_max_concurrent_streams,
15641            advertised,
15642            "MAX_CONCURRENT_STREAMS must be untouched when no burst occurred"
15643        );
15644    }
15645
15646    /// The RFC 9113 §6.5 SETTINGS-ACK deadline is evaluated against
15647    /// `ConnectionH2::now`. `flush_pending_control_frames` is the `writable`
15648    /// half of the pair; the `poll_read_target` half at `h2.rs` shares the predicate.
15649    ///
15650    /// To SEE THIS RED: restore `sent_at.elapsed() >= SETTINGS_ACK_TIMEOUT` in
15651    /// `flush_pending_control_frames`. No real time passes in this test, so the
15652    /// deadline never fires however far the connection's clock is advanced, and
15653    /// the third assertion fails with `advancing only the connection snapshot
15654    /// to the deadline must emit GOAWAY`.
15655    #[test]
15656    fn settings_ack_deadline_is_evaluated_against_the_connection_snapshot() {
15657        let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
15658        let (mut connection, _peer) = test_h2_connection(&pool, None);
15659
15660        let sent_at = connection.core.now;
15661        connection.core.settings_sent_at = Some(sent_at);
15662
15663        // One millisecond short of the budget on the connection's clock.
15664        connection.core.now = sent_at + SETTINGS_ACK_TIMEOUT - Duration::from_millis(1);
15665        assert!(
15666            matches!(
15667                connection.core.flush_pending_control_frames(),
15668                H2ControlFlushTarget::Proceed
15669            ),
15670            "the SETTINGS-ACK deadline must not fire before SETTINGS_ACK_TIMEOUT \
15671             on the connection's own clock"
15672        );
15673        assert_eq!(
15674            connection.core.settings_sent_at,
15675            Some(sent_at),
15676            "a deadline that has not fired must leave the ACK timer armed"
15677        );
15678
15679        // At the budget: GOAWAY(SETTINGS_TIMEOUT).
15680        connection.core.now = sent_at + SETTINGS_ACK_TIMEOUT;
15681        assert!(
15682            matches!(
15683                connection.core.flush_pending_control_frames(),
15684                H2ControlFlushTarget::Done(_)
15685            ),
15686            "advancing only the connection snapshot to the deadline must emit GOAWAY"
15687        );
15688        assert_eq!(
15689            connection.core.settings_sent_at, None,
15690            "goaway must disarm the ACK timer so the timeout cannot re-fire"
15691        );
15692    }
15693
15694    /// The per-stream liveness guard reaps against `ConnectionH2::now`, and
15695    /// `cancel_timed_out_streams` adopts `context.now` on entry — this pins
15696    /// both the entry-point mirror and the deadline arithmetic in
15697    /// `H2StreamTable::collect_timed_out`.
15698    ///
15699    /// To SEE THIS RED: restore `let now = Instant::now();` in
15700    /// `cancel_timed_out_streams` (in place of `let now = self.now;`). The
15701    /// injected instant is then ignored, no stream is ever old enough to reap
15702    /// because no real time passes, and the reap assertion fails with
15703    /// `advancing only the mux snapshot past the idle deadline must reap the
15704    /// stream`. Deleting the `self.now = context.now;` entry-point assignment
15705    /// turns the same assertion red, with the same message, for the same reason.
15706    #[test]
15707    fn per_stream_liveness_reaping_is_evaluated_against_the_connection_snapshot() {
15708        let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
15709        let (mut connection, _peer) = test_h2_connection(&pool, None);
15710        let mut context = test_context(&pool);
15711        let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
15712
15713        let idle_timeout = connection.core.stream_idle_timeout;
15714        let armed_at = connection.core.now;
15715        let gid = context
15716            .create_stream(Ulid::generate(), 1 << 16)
15717            .expect("test context must create a stream");
15718        connection.core.stream_table.register(1, gid, armed_at);
15719
15720        // Exactly at the deadline: the predicate is a strict `>`, so the stream
15721        // survives.
15722        context.now = armed_at + idle_timeout;
15723        connection.cancel_timed_out_streams(&mut context, &mut EndpointClient(&mut router));
15724        assert!(
15725            connection.core.control_tx.pending().is_empty(),
15726            "a stream exactly at its idle deadline must not be reaped"
15727        );
15728
15729        // One millisecond past it: reaped with RST_STREAM(CANCEL).
15730        context.now = armed_at + idle_timeout + Duration::from_millis(1);
15731        connection.cancel_timed_out_streams(&mut context, &mut EndpointClient(&mut router));
15732        assert!(
15733            !connection.core.control_tx.pending().is_empty(),
15734            "advancing only the mux snapshot past the idle deadline must reap the stream"
15735        );
15736        assert_eq!(
15737            connection.core.control_tx.pending()[0],
15738            (1, H2Error::Cancel),
15739            "the reaper must queue RST_STREAM(CANCEL) for the timed-out stream"
15740        );
15741    }
15742
15743    /// A mass idle-timeout reap is the only path that queues more than one
15744    /// RST_STREAM between two `flush_pending_control_frames` passes:
15745    /// `cancel_timed_out_streams` walks the whole timed-out set and calls
15746    /// `enqueue_rst` for every entry inside ONE pass. The cap therefore has to
15747    /// live at the insert, not at the drain — a drain-side check bounds what
15748    /// is *written*, and says nothing about how large the queue got before
15749    /// anyone looked. See sozu-proxy/sozu#1413.
15750    ///
15751    /// `h2_control_tx::pending_rst_bound` keeps the bound above four resets
15752    /// per advertised concurrent stream, so a real sweep cannot reach it on
15753    /// its own; the queue still holds what every caller queued since the last
15754    /// successful drain. This test registers more wire ids than the bound on
15755    /// the stream table directly, exactly as its sibling
15756    /// `per_stream_liveness_reaping_is_evaluated_against_the_connection_snapshot`
15757    /// does, because the accept path is not what is under test here.
15758    ///
15759    /// `check_invariants` is asserted explicitly because the reaper is also
15760    /// reachable from `Mux::timeout` against a fully silent peer — the very
15761    /// scenario that produces a mass reap — where `handle_frame`, the only
15762    /// production caller of `check_invariants`, never runs at all. The reaper
15763    /// is not the only insert path outside that caller: the
15764    /// DATA-on-closed-stream reset sits in `handle_header_state`, which
15765    /// `handle_read` returns into without ever reaching `handle_frame`. That one
15766    /// is rate-limited by `record_glitch` + `check_flood_or_return!` and
15767    /// cannot by itself fill the queue — see `ConnectionH2::enqueue_rst`.
15768    ///
15769    /// To SEE THIS RED: delete the `pending_before >= self.max_pending` guard
15770    /// at the top of `H2ControlTx::enqueue_rst` (`h2_control_tx.rs`), then run
15771    /// `cargo test -p sozu-lib --locked mass_reap` (one positional filter —
15772    /// cargo rejects a second one). Every reaped stream is then queued
15773    /// unconditionally and `H2ControlTx::check_invariants` — the post-condition
15774    /// every mutating method on that type runs — panics inside the sweep with
15775    /// `pending RST queue must stay within its hard cap (escalates at the
15776    /// cap)`. That clause moved to the module with this commit and is NOT
15777    /// restated in `ConnectionH2::check_invariants`, so the explicit call
15778    /// below no longer covers the queue bound — the assertion at the end of
15779    /// this test is what pins it from the connection's side.
15780    #[test]
15781    fn mass_reap_keeps_the_pending_rst_queue_within_its_hard_cap() {
15782        // `Stream::new` checks out two buffers per stream and the connection
15783        // itself holds one for its zero buffer, so the ceiling has to clear
15784        // `2 * REAPED` with room to spare or `create_stream` hands back
15785        // `None` — which is how the first draft of this test failed.
15786        //
15787        // A production bound (`h2_control_tx::pending_rst_bound`, at least
15788        // `MIN_PENDING_RST_STREAMS`) would need thousands of streams and two
15789        // pool buffers each; what is under test is that the queue honours
15790        // its instance's bound, so the test installs a small one.
15791        let bound = 200;
15792        let reaped = bound + 32;
15793        let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 2 * reaped + 4, 16384)));
15794        let (mut connection, _peer) = test_h2_connection(&pool, None);
15795        connection.core.control_tx = h2_control_tx::H2ControlTx::with_cap(bound);
15796        let mut context = test_context(&pool);
15797        let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
15798
15799        let idle_timeout = connection.core.stream_idle_timeout;
15800        let armed_at = connection.core.now;
15801        for i in 0..reaped {
15802            let gid = context
15803                .create_stream(Ulid::generate(), 1 << 16)
15804                .expect("test context must create a stream");
15805            // RFC 9113 §5.1.1: client-initiated stream ids are odd.
15806            connection
15807                .core
15808                .stream_table
15809                .register(2 * i as StreamId + 1, gid, armed_at);
15810        }
15811
15812        // One sweep, every stream past its deadline: the entire set is reaped
15813        // and RST-queued with no intervening flush.
15814        context.now = armed_at + idle_timeout + Duration::from_millis(1);
15815        connection.cancel_timed_out_streams(&mut context, &mut EndpointClient(&mut router));
15816
15817        #[cfg(debug_assertions)]
15818        connection.core.check_invariants(&context);
15819        assert!(
15820            connection.core.control_tx.pending().len() <= bound,
15821            "a mass reap must stop queueing at the cap, got {} entries",
15822            connection.core.control_tx.pending().len()
15823        );
15824    }
15825
15826    // ── flood accounting: refusals, provoked resets, the denominator ────
15827
15828    /// A peer that keeps opening streams Sōzu refuses neither grows the
15829    /// connection's state nor escapes the flood detector: a refused stream
15830    /// was never registered, so nothing ever removes its id from `rst_sent`,
15831    /// and each refusal past the limit counts as a glitch.
15832    ///
15833    /// The refusals here fall in the band back-pressure opens, local limit
15834    /// lowered below the advertised one, which cost nothing before.
15835    #[test]
15836    fn refused_streams_keep_rst_sent_bounded_and_trip_the_glitch_budget() {
15837        use std::io::Write;
15838
15839        const BATCHES: u32 = 50;
15840        const PER_BATCH: u32 = 1000;
15841        let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 8, 16384)));
15842        let (mut connection, mut peer) = test_h2_connection(&pool, None);
15843        let mut context = test_context(&pool);
15844        let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
15845        connection.core.state = H2State::Header;
15846        connection
15847            .core
15848            .stream_table
15849            .set_expect_read(Some((H2StreamId::Zero, 9)));
15850        connection
15851            .core
15852            .local_settings
15853            .settings_max_concurrent_streams = 0;
15854        // `:method GET`, `:path /`, `:scheme https`, then `:authority`
15855        // as a literal without indexing: no HPACK table state to track.
15856        let mut block = vec![0x82, 0x84, 0x87, 0x01, 0x09];
15857        block.extend_from_slice(b"localhost");
15858
15859        let mut next_id: u32 = 1;
15860        'batches: for _ in 0..BATCHES {
15861            let mut wire = Vec::new();
15862            for _ in 0..PER_BATCH {
15863                wire.extend(orphan_frame(
15864                    1,
15865                    parser::FLAG_END_HEADERS | parser::FLAG_END_STREAM,
15866                    next_id,
15867                    block.len() as u32,
15868                    &block,
15869                ));
15870                next_id += 2;
15871            }
15872            peer.write_all(&wire).expect("loopback write must complete");
15873            peer.flush().expect("loopback flush must complete");
15874            // One read pass handles one frame header or one payload: drive
15875            // until the batch's last stream id has been seen.
15876            let last_id = next_id - 2;
15877            for _ in 0..(16 * PER_BATCH) {
15878                connection.core.readiness.event.insert(Ready::READABLE);
15879                connection.core.readiness.interest.insert(Ready::READABLE);
15880                connection.readable(&mut context, EndpointClient(&mut router));
15881                let _ = connection.core.flush_pending_control_frames();
15882                connection.core.clear_output();
15883                if matches!(connection.core.state, H2State::GoAway | H2State::Error) {
15884                    break 'batches;
15885                }
15886                if connection.core.stream_table.highest_peer_stream_id() >= last_id
15887                    && matches!(connection.core.state, H2State::Header)
15888                {
15889                    break;
15890                }
15891            }
15892        }
15893
15894        assert!(
15895            connection.core.stream_table.rst_sent_mut().len() < 100,
15896            "refused stream ids must not accumulate in rst_sent, got {}",
15897            connection.core.stream_table.rst_sent_mut().len()
15898        );
15899        assert!(
15900            matches!(connection.core.state, H2State::GoAway | H2State::Error),
15901            "endless refusals must trip the flood detector, got {:?} after {} streams",
15902            connection.core.state,
15903            next_id / 2
15904        );
15905    }
15906
15907    /// Every reset the peer provokes is a protocol error on its part and
15908    /// counts as a glitch, so provoked resets have a per-window rate limit
15909    /// independent of how many streams the connection served.
15910    #[test]
15911    fn peer_provoked_resets_count_as_glitches() {
15912        let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
15913        let (mut connection, _peer) = test_h2_connection(&pool, None);
15914        let now = connection.core.now;
15915        for _ in 0..1_000_000 {
15916            connection.core.flood_detector.record_stream_opened();
15917        }
15918        let glitch_budget = H2FloodConfig::default().max_glitch_count();
15919        let mut tripped_at = None;
15920        for n in 1..=(glitch_budget + 1) {
15921            connection.core.now = now;
15922            if connection
15923                .core
15924                .account_emitted_rst(H2Error::ProtocolError, RstOrigin::PeerProvoked)
15925                .is_some()
15926            {
15927                tripped_at = Some(n);
15928                break;
15929            }
15930        }
15931        assert_eq!(
15932            tripped_at,
15933            Some(glitch_budget + 1),
15934            "the provoked reset past the glitch budget must end the connection"
15935        );
15936    }
15937
15938    /// Streams the peer opens count toward the RST caps' denominator only
15939    /// once a backend answers them: a stream Sōzu refuses, answers itself or
15940    /// sees reset before a response cannot dilute the caps.
15941    #[test]
15942    fn unanswered_streams_do_not_dilute_the_reset_caps() {
15943        const FLOOR: u64 = 10;
15944        let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 70, 16384)));
15945        let (mut connection, _peer) = test_h2_connection(&pool, None);
15946        let mut context = test_context(&pool);
15947        connection.core.flood_detector = h2_flood_detector::H2FloodDetector::new(
15948            H2FloodConfig::from_optional(
15949                None,
15950                None,
15951                None,
15952                None,
15953                None,
15954                None,
15955                None,
15956                None,
15957                Some(FLOOR),
15958                None,
15959                None,
15960                None,
15961                None,
15962                None,
15963            ),
15964            connection.core.now,
15965        );
15966        for i in 0..30u32 {
15967            connection
15968                .core
15969                .create_stream(2 * i + 1, &mut context)
15970                .expect("the pool must hand out stream buffers");
15971        }
15972        let mut tripped_at = None;
15973        for n in 1..=(FLOOR + 1) {
15974            if connection
15975                .core
15976                .flood_detector
15977                .record_rst_received(false, false, connection.core.now)
15978                .is_some()
15979            {
15980                tripped_at = Some(n);
15981                break;
15982            }
15983        }
15984        assert_eq!(
15985            tripped_at,
15986            Some(FLOOR + 1),
15987            "thirty unanswered streams must not raise the pre-response cap"
15988        );
15989    }
15990
15991    /// Resets Sōzu emits on its own initiative — here the idle reaper's
15992    /// `RST_STREAM(CANCEL)` — are not charged to the peer: neither the
15993    /// CVE-2025-8671 MadeYouReset emitted-RST counter nor the pending RST
15994    /// queue's bound may accumulate them across drains.
15995    ///
15996    /// Two sweeps of 150 reaped streams with a control flush between them:
15997    /// 300 local resets in the connection's lifetime, never more than 150
15998    /// pending at once.
15999    ///
16000    /// TO SEE THIS RED: in `ConnectionH2::cancel_timed_out_streams`, pass
16001    /// `RstOrigin::PeerProvoked` instead of `RstOrigin::Local` to
16002    /// `ConnectionH2::enqueue_rst`. The emitted-RST counter then reads 300.
16003    #[test]
16004    fn local_resets_are_not_charged_to_the_peer() {
16005        const PER_SWEEP: usize = 150;
16006        let pool = Rc::new(RefCell::new(Pool::with_capacity(
16007            2,
16008            4 * PER_SWEEP + 4,
16009            16384,
16010        )));
16011        let (mut connection, _peer) = test_h2_connection(&pool, None);
16012        let mut context = test_context(&pool);
16013        let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
16014        let idle_timeout = connection.core.stream_idle_timeout;
16015
16016        for sweep in 0..2 {
16017            let armed_at = connection.core.now;
16018            for i in 0..PER_SWEEP {
16019                let gid = context
16020                    .create_stream(Ulid::generate(), 1 << 16)
16021                    .expect("test context must create a stream");
16022                let wire_id = 2 * (sweep * PER_SWEEP + i) as StreamId + 1;
16023                connection
16024                    .core
16025                    .stream_table
16026                    .register(wire_id, gid, armed_at);
16027            }
16028            context.now = armed_at + idle_timeout + Duration::from_millis(1);
16029            connection.cancel_timed_out_streams(&mut context, &mut EndpointClient(&mut router));
16030            // The SETTINGS-ACK watchdog is not under test.
16031            connection.core.settings_sent_at = None;
16032            let flushed = connection.core.flush_pending_control_frames();
16033            assert!(
16034                !matches!(flushed, H2ControlFlushTarget::Done(_)),
16035                "sweep {sweep}: local resets must not end the connection"
16036            );
16037            assert!(
16038                !matches!(connection.core.state, H2State::GoAway | H2State::Error),
16039                "sweep {sweep}: local resets must not send GOAWAY"
16040            );
16041        }
16042        assert_eq!(
16043            connection
16044                .core
16045                .flood_detector
16046                .total_rst_streams_emitted_lifetime(),
16047            0,
16048            "reaper CANCELs are Sōzu's decision, not the peer's"
16049        );
16050    }
16051
16052    // ── forcefully_terminate_answer arms WRITABLE for ET epoll ───────────
16053    //
16054    // Gap A in the h2spec diagnosis: the pre-fix code set `interest` but
16055    // never raised `event`, so `filter_interest() = event & interest` was
16056    // zero and `writable()` was never scheduled. This test pins the fix.
16057
16058    #[test]
16059    fn test_forcefully_terminate_answer_arms_event_and_interest() {
16060        let pool = make_pool_for_invariant_16();
16061        let ulid = Ulid::generate();
16062        let mut stream = make_stream_for_invariant_16(&pool, ulid);
16063        let mut readiness = Readiness::new();
16064
16065        assert!(!readiness.interest.is_writable());
16066        assert!(!readiness.event.is_writable());
16067
16068        forcefully_terminate_answer(&mut stream, &mut readiness, H2Error::ProtocolError);
16069
16070        assert!(
16071            readiness.interest.is_writable(),
16072            "forcefully_terminate_answer must set the WRITABLE interest bit"
16073        );
16074        assert!(
16075            readiness.event.is_writable(),
16076            "forcefully_terminate_answer must set the WRITABLE event bit — \
16077             without this, filter_interest() = 0 under edge-triggered epoll \
16078             and writable() is never scheduled (h2spec Gap A)"
16079        );
16080    }
16081
16082    // ── peer-address snapshot in the MUX-H2 log envelope ─────────────────
16083    //
16084    // `log_context!` / `log_context_stream!` render `peer=` from
16085    // `SocketHandler::peer_addr`, which is a snapshot the handler cached, not a
16086    // live `getpeername(2)`. Both tests below make the two answers differ on
16087    // purpose: the socket is genuinely connected to a loopback listener, so the
16088    // live lookup succeeds and would print `127.0.0.1:<port>`; only a macro
16089    // reading the snapshot can print `10.0.0.42:12345`.
16090    //
16091    // That gap is the PROXY-protocol symptom in miniature — the frontend's real
16092    // peer is the load balancer while the cache holds the advertised client —
16093    // and the same read is what keeps the slot populated after the peer's RST,
16094    // when `getpeername(2)` answers ENOTCONN and the live lookup collapses to
16095    // `None` on exactly the error lines an operator is reading.
16096
16097    /// A live, established loopback connection plus the address
16098    /// `getpeername(2)` reports for it. The listener is returned so the
16099    /// connection stays up for the whole test: these tests assert the snapshot
16100    /// wins even while the live lookup is perfectly healthy, so a half-dead
16101    /// socket would weaken them rather than strengthen them.
16102    fn connected_loopback_stream() -> (
16103        std::net::TcpListener,
16104        mio::net::TcpStream,
16105        std::net::SocketAddr,
16106    ) {
16107        let listener = std::net::TcpListener::bind("127.0.0.1:0")
16108            .expect("test listener must bind to a loopback port");
16109        let live_peer = listener
16110            .local_addr()
16111            .expect("test listener must report its local address");
16112        let stream =
16113            std::net::TcpStream::connect(live_peer).expect("loopback connect must complete");
16114        stream
16115            .set_nonblocking(true)
16116            .expect("mio requires a nonblocking stream");
16117        (listener, mio::net::TcpStream::from_std(stream), live_peer)
16118    }
16119
16120    /// Address the handler caches. Deliberately non-loopback so it cannot
16121    /// collide with whatever ephemeral port the live lookup would report.
16122    const CACHED_PEER: &str = "10.0.0.42:12345";
16123
16124    /// Build a server-side H2 connection whose frontend handler caches
16125    /// [`CACHED_PEER`] while its socket is really connected to `live_peer`.
16126    fn connection_with_cached_peer(
16127        pool: &Rc<RefCell<Pool>>,
16128        stream: mio::net::TcpStream,
16129    ) -> H2Shell<crate::socket::SessionTcpStream> {
16130        let session_ulid = Ulid::generate();
16131        let socket = crate::socket::SessionTcpStream::new(
16132            stream,
16133            session_ulid,
16134            Some(
16135                CACHED_PEER
16136                    .parse()
16137                    .expect("the cached peer literal must parse"),
16138            ),
16139        );
16140        H2Shell::new(
16141            session_ulid,
16142            socket,
16143            Position::Server,
16144            &mut PoolBufferSource::new(Rc::downgrade(pool)),
16145            H2FloodConfig::default(),
16146            H2ConnectionConfig::default(),
16147            Duration::from_secs(30),
16148            None,
16149            Duration::from_secs(30),
16150            Some((H2StreamId::Zero, CLIENT_PREFACE_SIZE)),
16151            Ready::READABLE | Ready::HUP | Ready::ERROR,
16152        )
16153        .expect("a pool with free buffers must yield an H2 connection")
16154    }
16155
16156    /// To SEE THIS RED: in `log_context!` (h2.rs), put
16157    /// `peer = $self.socket.socket_ref().peer_addr().ok(),` back in place of
16158    /// `peer = $self.peer_address,`. The rendered line then carries the
16159    /// loopback address the socket is really connected to, so the first
16160    /// assertion fails on the missing cached address.
16161    ///
16162    /// Substituting the pre-snapshot `peer = $self.socket.peer_addr(),` does
16163    /// NOT turn this red, and that is worth stating: for a handler that
16164    /// already caches, reading the cache and reading a snapshot taken from
16165    /// that cache agree by construction. What the snapshot changed is WHICH
16166    /// object is read on a log line, not what comes back — so the call-count
16167    /// property, not this one, is what distinguishes them. It is pinned by
16168    /// [`log_context_reads_the_peer_address_once_per_connection`].
16169    #[test]
16170    fn log_context_renders_the_cached_peer_not_a_live_lookup() {
16171        let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
16172        let (_listener, stream, live_peer) = connected_loopback_stream();
16173        let connection = connection_with_cached_peer(&pool, stream);
16174
16175        // Premise of the test: the live lookup is healthy and disagrees with the
16176        // cache. Without this the assertions below could pass for the wrong
16177        // reason (both answers happening to be the same address).
16178        assert_eq!(
16179            connection.socket.socket_ref().peer_addr().ok(),
16180            Some(live_peer),
16181            "the test socket must be genuinely connected, so a live lookup succeeds"
16182        );
16183
16184        let rendered = log_context!(connection.core);
16185
16186        assert!(
16187            rendered.contains(&format!("peer=Some({CACHED_PEER})")),
16188            "MUX-H2 context must render the cached peer: {rendered}"
16189        );
16190        assert!(
16191            !rendered.contains(&live_peer.to_string()),
16192            "MUX-H2 context must not fall back to the live getpeername(2) answer: {rendered}"
16193        );
16194    }
16195
16196    /// To SEE THIS RED: in `log_context_stream!` (h2.rs), put
16197    /// `peer = $self.socket.socket_ref().peer_addr().ok(),` back in place of
16198    /// `peer = $self.peer_address,`. The per-stream envelope then
16199    /// diverges from the connection envelope, which is worse than either being
16200    /// wrong alone: the same session renders two different peers depending on
16201    /// whether a stream happened to be in scope at the callsite.
16202    #[test]
16203    fn log_context_stream_renders_the_cached_peer_not_a_live_lookup() {
16204        let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 20, 16_384)));
16205        let (_listener, stream, live_peer) = connected_loopback_stream();
16206        let connection = connection_with_cached_peer(&pool, stream);
16207        let mux_stream = make_stream_for_invariant_16(&pool, connection.core.session_ulid);
16208        let http_context = &mux_stream.context;
16209
16210        assert_eq!(
16211            connection.socket.socket_ref().peer_addr().ok(),
16212            Some(live_peer),
16213            "the test socket must be genuinely connected, so a live lookup succeeds"
16214        );
16215
16216        let rendered = log_context_stream!(connection.core, http_context);
16217
16218        assert!(
16219            rendered.contains(&format!("peer=Some({CACHED_PEER})")),
16220            "per-stream MUX-H2 context must render the cached peer: {rendered}"
16221        );
16222        assert!(
16223            !rendered.contains(&live_peer.to_string()),
16224            "per-stream MUX-H2 context must not fall back to the live \
16225             getpeername(2) answer: {rendered}"
16226        );
16227    }
16228
16229    /// A [`SocketHandler`] that counts how often it is asked for the peer
16230    /// address, delegating every other method to a real loopback stream.
16231    ///
16232    /// The two tests above pin WHICH address the log prefix renders. Neither
16233    /// can see how many times the connection reaches into the socket to get
16234    /// it, because both production handlers answer from a cache and so give
16235    /// the same answer however often they are asked. This handler makes the
16236    /// count observable.
16237    struct PeerAddrCountingSocket {
16238        stream: mio::net::TcpStream,
16239        peer: std::net::SocketAddr,
16240        calls: Rc<std::cell::Cell<usize>>,
16241    }
16242
16243    impl SocketHandler for PeerAddrCountingSocket {
16244        fn socket_read(&mut self, buf: &mut [u8]) -> (usize, SocketResult) {
16245            self.stream.socket_read(buf)
16246        }
16247
16248        fn socket_write(&mut self, buf: &[u8]) -> (usize, SocketResult) {
16249            self.stream.socket_write(buf)
16250        }
16251
16252        fn socket_write_vectored(&mut self, bufs: &[IoSlice]) -> (usize, SocketResult) {
16253            self.stream.socket_write_vectored(bufs)
16254        }
16255
16256        fn socket_ref(&self) -> &mio::net::TcpStream {
16257            &self.stream
16258        }
16259
16260        fn socket_mut(&mut self) -> &mut mio::net::TcpStream {
16261            &mut self.stream
16262        }
16263
16264        fn peer_addr(&self) -> Option<std::net::SocketAddr> {
16265            self.calls.set(self.calls.get() + 1);
16266            Some(self.peer)
16267        }
16268
16269        fn protocol(&self) -> crate::socket::TransportProtocol {
16270            crate::socket::TransportProtocol::Tcp
16271        }
16272
16273        fn read_error(&self) {}
16274
16275        fn write_error(&self) {}
16276    }
16277
16278    /// The `peer=` slot is read from the socket exactly ONCE per connection —
16279    /// at construction — however many log lines the connection renders.
16280    ///
16281    /// This is the property that makes `peer_address` worth having. It is not
16282    /// about which address appears (the two tests above own that); it is about
16283    /// `log_context!` no longer being a socket operation. The macro expands at
16284    /// 113 production callsites in this file, so before the snapshot every
16285    /// method that logged was `Front`-coupled whether or not it did any I/O —
16286    /// which is 113 of the ~140 total `Front` touch points in `h2.rs`.
16287    ///
16288    /// TO SEE THIS RED: in `log_context!` (h2.rs), put
16289    /// `peer = $self.socket.peer_addr(),` back in place of
16290    /// `peer = $self.peer_address,`. The count then rises by one per rendered
16291    /// line and the final assertion fails with
16292    /// `the peer address must be read once, at construction, not once per log line:
16293    /// left: 4, right: 1`.
16294    #[test]
16295    fn log_context_reads_the_peer_address_once_per_connection() {
16296        let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
16297        let (_listener, stream, _live_peer) = connected_loopback_stream();
16298        let calls = Rc::new(std::cell::Cell::new(0usize));
16299        let session_ulid = Ulid::generate();
16300        let socket = PeerAddrCountingSocket {
16301            stream,
16302            peer: CACHED_PEER
16303                .parse()
16304                .expect("the cached peer literal must parse"),
16305            calls: Rc::clone(&calls),
16306        };
16307        let connection = H2Shell::new(
16308            session_ulid,
16309            socket,
16310            Position::Server,
16311            &mut PoolBufferSource::new(Rc::downgrade(&pool)),
16312            H2FloodConfig::default(),
16313            H2ConnectionConfig::default(),
16314            Duration::from_secs(30),
16315            None,
16316            Duration::from_secs(30),
16317            Some((H2StreamId::Zero, CLIENT_PREFACE_SIZE)),
16318            Ready::READABLE | Ready::HUP | Ready::ERROR,
16319        )
16320        .expect("a pool with free buffers must yield an H2 connection");
16321
16322        assert_eq!(
16323            calls.get(),
16324            1,
16325            "construction takes exactly one peer_addr() snapshot"
16326        );
16327
16328        // Premise: the rendered lines really do carry the address, so a zero
16329        // count below would mean the slot went missing rather than that it
16330        // became free.
16331        for _ in 0..3 {
16332            let rendered = log_context!(connection.core);
16333            assert!(
16334                rendered.contains(&format!("peer=Some({CACHED_PEER})")),
16335                "each rendered line must still carry the peer: {rendered}"
16336            );
16337        }
16338
16339        assert_eq!(
16340            calls.get(),
16341            1,
16342            "the peer address must be read once, at construction, not once per log line"
16343        );
16344    }
16345
16346    // ── The read-side two-call protocol (poll_read_target / handle_read) ──
16347
16348    /// `read_space` hands out the buffer the core named, and hands out the
16349    /// stream's READ buffer — the one `Stream::split` labels `rbuffer` for the
16350    /// connection's position — not its write buffer.
16351    ///
16352    /// This is the only genuinely new logic in the read-side inversion: every
16353    /// other line moved. Both halves are pinned because the two failure modes
16354    /// differ — offering the wrong *kawa* corrupts the peer's request while
16355    /// the response is parsed as H2 payload, offering the wrong *stream*
16356    /// writes one stream's DATA into another's.
16357    ///
16358    /// To SEE THIS RED: in `read_buffer` (h2.rs), return
16359    /// `streams[global_stream_id].split(position).wbuffer` in place of
16360    /// `.rbuffer`.
16361    #[test]
16362    fn read_space_offers_the_read_buffer_of_the_stream_the_core_named() {
16363        let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 20, 16_384)));
16364        let (mut connection, _peer) = test_h2_connection(&pool, None);
16365        let mut context = test_context(&pool);
16366
16367        let gid = context
16368            .create_stream(Ulid::generate(), 1 << 16)
16369            .expect("test context must create a stream");
16370
16371        // Premise: the two per-stream buffers really are distinct allocations,
16372        // so a pointer comparison below can tell them apart at all.
16373        let front = context.streams[gid].front.storage.space().as_ptr();
16374        let back = context.streams[gid].back.storage.space().as_ptr();
16375        let zero = connection.core.zero.storage.space().as_ptr();
16376        assert_ne!(
16377            front, back,
16378            "a stream's request and response buffers must be distinct allocations"
16379        );
16380
16381        let offered = read_space(
16382            &mut connection.core.zero,
16383            &mut context.streams,
16384            &connection.core.position,
16385            H2StreamId::Zero,
16386            9,
16387        );
16388        assert_eq!(
16389            offered.as_ptr(),
16390            zero,
16391            "H2StreamId::Zero must offer the connection-level scratch buffer"
16392        );
16393        assert_eq!(
16394            offered.len(),
16395            9,
16396            "the offer is capped at the byte debt, never the whole free space"
16397        );
16398
16399        let offered = read_space(
16400            &mut connection.core.zero,
16401            &mut context.streams,
16402            &connection.core.position,
16403            H2StreamId::Other { id: 1, gid },
16404            7,
16405        );
16406        assert_eq!(
16407            offered.as_ptr(),
16408            front,
16409            "a server position must offer the stream's request (front) buffer"
16410        );
16411        assert_ne!(
16412            offered.as_ptr(),
16413            back,
16414            "the offered buffer must not be the stream's response (back) buffer"
16415        );
16416        assert_eq!(
16417            offered.len(),
16418            7,
16419            "the offer is capped at the byte debt, never the whole free space"
16420        );
16421    }
16422
16423    /// A frame whose payload is zero bytes long owes the socket nothing, and
16424    /// the core says so with its own variant rather than an `amount` of 0.
16425    ///
16426    /// That distinction is load-bearing, not cosmetic. A caller handed
16427    /// `Fill { amount: 0 }` would issue a zero-length read, and
16428    /// `update_readiness_after_read(0, SocketResult::Continue, ..)` returns
16429    /// `true` for it — "nothing arrived, stop" — so `handle_read` would return
16430    /// before the frame state machine ever ran, and every empty SETTINGS,
16431    /// empty DATA and SETTINGS ACK would stop being parsed.
16432    ///
16433    /// To SEE THIS RED: in `poll_read_target` (h2.rs), replace the trailing
16434    /// `if amount > 0 { .. } else { .. }` with its `Fill` half alone —
16435    /// keep the `available_space` guard and end the function with
16436    /// `H2ReadTarget::Fill { stream_id, amount }`, dropping the
16437    /// `set_expect_read(None)` / `H2ReadTarget::Skip(stream_id)` branch.
16438    #[test]
16439    fn poll_read_target_skips_the_read_when_the_frame_carries_no_payload() {
16440        let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
16441        let (mut connection, _peer) = test_h2_connection(&pool, None);
16442        let mut context = test_context(&pool);
16443        let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
16444
16445        connection.core.state = H2State::Header;
16446        connection
16447            .core
16448            .stream_table
16449            .set_expect_read(Some((H2StreamId::Zero, 0)));
16450
16451        let target = connection
16452            .core
16453            .poll_read_target(&mut context, &mut EndpointClient(&mut router));
16454        assert!(
16455            matches!(target, H2ReadTarget::Skip(H2StreamId::Zero)),
16456            "a zero-length payload must be a Skip, not a zero-length Fill: {target:?}"
16457        );
16458        assert!(
16459            connection.core.stream_table.expect_read().is_none(),
16460            "the settled byte debt must be cleared before the frame is dispatched"
16461        );
16462    }
16463
16464    /// The core is told how many bytes arrived, and it subtracts exactly that
16465    /// many from the debt it offered — a short read leaves the remainder owed,
16466    /// it does not restart the frame.
16467    ///
16468    /// 6 is neither of the two numbers the test hands in, so this cannot pass
16469    /// by echoing the fixture: `amount` is 9 and `size` is 3.
16470    ///
16471    /// To SEE THIS RED: in `handle_read` (h2.rs), pass `amount` in place of
16472    /// `amount - size` to `set_expect_read`.
16473    #[test]
16474    fn handle_read_subtracts_the_byte_count_the_caller_reported() {
16475        let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
16476        let (mut connection, _peer) = test_h2_connection(&pool, None);
16477        let mut context = test_context(&pool);
16478        let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
16479
16480        // Not ClientPreface: a short read there runs the early-preface check
16481        // instead, which is a different branch with its own coverage.
16482        connection.core.state = H2State::Header;
16483        connection
16484            .core
16485            .stream_table
16486            .set_expect_read(Some((H2StreamId::Zero, 9)));
16487
16488        let result = connection.core.handle_read(
16489            &mut context,
16490            EndpointClient(&mut router),
16491            H2StreamId::Zero,
16492            H2ReadOutcome::Filled {
16493                amount: 9,
16494                size: 3,
16495                status: SocketResult::Continue,
16496            },
16497        );
16498
16499        assert!(
16500            matches!(result, MuxResult::Continue),
16501            "a short read keeps the connection running: {result:?}"
16502        );
16503        assert_eq!(
16504            connection.core.stream_table.expect_read(),
16505            Some((H2StreamId::Zero, 6)),
16506            "a 3-byte answer to a 9-byte offer must leave 6 bytes owed"
16507        );
16508        assert_eq!(
16509            connection.core.zero.storage.data().len(),
16510            3,
16511            "the core must consume exactly the bytes the caller reported"
16512        );
16513    }
16514
16515    /// A read that returned nothing clears the READABLE **event** and leaves
16516    /// the READABLE **interest** alone, so the connection is re-armed for the
16517    /// next epoll wake-up instead of being taken off the loop.
16518    ///
16519    /// The interest half is asserted on purpose: `signal_pending_write` and
16520    /// this path both touch `Readiness.event` only, and an assertion written
16521    /// against `interest` cannot see either of them move.
16522    ///
16523    /// To SEE THIS RED: in `handle_read` (h2.rs), call
16524    /// `update_readiness_after_write` in place of `update_readiness_after_read`
16525    /// — the WRITABLE bit is cleared instead and the READABLE event survives.
16526    #[test]
16527    fn handle_read_clears_the_readable_event_not_the_interest_when_nothing_arrived() {
16528        let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
16529        let (mut connection, _peer) = test_h2_connection(&pool, None);
16530        let mut context = test_context(&pool);
16531        let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
16532
16533        connection.core.state = H2State::Header;
16534        connection
16535            .core
16536            .stream_table
16537            .set_expect_read(Some((H2StreamId::Zero, 9)));
16538        connection.core.readiness.event = Ready::READABLE;
16539        connection.core.readiness.interest = Ready::READABLE | Ready::HUP | Ready::ERROR;
16540
16541        let result = connection.core.handle_read(
16542            &mut context,
16543            EndpointClient(&mut router),
16544            H2StreamId::Zero,
16545            H2ReadOutcome::Filled {
16546                amount: 9,
16547                size: 0,
16548                status: SocketResult::WouldBlock,
16549            },
16550        );
16551
16552        assert!(
16553            matches!(result, MuxResult::Continue),
16554            "an empty read is not a close: {result:?}"
16555        );
16556        assert!(
16557            !connection.core.readiness.event.is_readable(),
16558            "an empty read must clear the READABLE event"
16559        );
16560        assert!(
16561            connection.core.readiness.interest.is_readable(),
16562            "an empty read must NOT drop the READABLE interest"
16563        );
16564        assert_eq!(
16565            connection.core.stream_table.expect_read(),
16566            Some((H2StreamId::Zero, 9)),
16567            "an empty read settles no part of the byte debt"
16568        );
16569    }
16570
16571    // ── RFC 9113 §4.3: a refused stream must not desynchronise HPACK ──────
16572    //
16573    // Field compression state is scoped to the whole connection, not to a
16574    // stream. When `handle_header_state` refuses a stream — during a
16575    // graceful drain, over MAX_CONCURRENT_STREAMS, or on buffer-pool
16576    // exhaustion — the HEADERS payload it drops is an HPACK field block the
16577    // peer's encoder has *already* applied to its own dynamic table. Dropping
16578    // it without decoding leaves our decoder permanently behind the peer's
16579    // encoder, and every later header block on the surviving connection then
16580    // resolves the wrong dynamic entry or fails outright.
16581
16582    /// To SEE THIS RED: in the `(H2State::Discard, _)` arm of
16583    /// [`ConnectionH2::handle_read`], remove the `if let Some(discarded) =
16584    /// self.discarded_field_block.take() && ...` block that calls
16585    /// [`decode_discarded_field_block`], restoring the unconditional
16586    /// `kawa.storage.clear()`. The peer's second block is then a one-byte
16587    /// reference to dynamic index 62 that our decoder has never been told
16588    /// about, and the decode fails with an out-of-table index. Verified
16589    /// 2026-09-21 (see this commit's message for the exact failure output).
16590    #[test]
16591    fn a_refused_stream_keeps_the_hpack_decoder_in_sync() {
16592        use std::io::Write;
16593
16594        let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 20, 16_384)));
16595        let (mut connection, mut peer) = test_h2_connection(&pool, None);
16596        let mut context = test_context(&pool);
16597        let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
16598
16599        // Past the preface/SETTINGS handshake, waiting on a frame header, and
16600        // draining — so the next client-initiated stream is refused rather
16601        // than created. Drain is only the cheapest of the three refusal
16602        // triggers; MAX_CONCURRENT_STREAMS and pool exhaustion reach the same
16603        // `refuse_stream_and_discard` call.
16604        connection.core.state = H2State::Header;
16605        connection
16606            .core
16607            .stream_table
16608            .set_expect_read(Some((H2StreamId::Zero, 9)));
16609        connection.core.drain.__test_set_draining();
16610
16611        // The peer's encoder. It indexes a header whose *name* is in
16612        // neither table, so this block appends `x-sozu-probe: alpha` to the
16613        // peer's dynamic table at index 62.
16614        let mut peer_encoder = crate::protocol::mux::hpack::Encoder::new();
16615        let refused_block = peer_encoder.encode([
16616            (&b":method"[..], &b"GET"[..]),
16617            (&b":scheme"[..], &b"https"[..]),
16618            (&b":authority"[..], &b"example.com"[..]),
16619            (&b":path"[..], &b"/refused"[..]),
16620            (&b"x-sozu-probe"[..], &b"alpha"[..]),
16621        ]);
16622        assert!(
16623            refused_block.len() < 16_384,
16624            "the probe block must fit one HEADERS frame under the default max frame size"
16625        );
16626
16627        // HEADERS, END_STREAM | END_HEADERS, stream 1.
16628        let mut frame = Vec::with_capacity(9 + refused_block.len());
16629        frame.extend_from_slice(&(refused_block.len() as u32).to_be_bytes()[1..]);
16630        frame.push(1);
16631        frame.push(parser::FLAG_END_STREAM | parser::FLAG_END_HEADERS);
16632        frame.extend_from_slice(&1u32.to_be_bytes());
16633        frame.extend_from_slice(&refused_block);
16634        peer.write_all(&frame)
16635            .expect("loopback write must complete");
16636        peer.flush().expect("loopback flush must complete");
16637
16638        // Two passes consume the 9-byte header and then the payload; the rest
16639        // are `WouldBlock` no-ops. Bounded so a delivery hiccup fails loudly
16640        // instead of hanging.
16641        for _ in 0..64 {
16642            connection.readable(&mut context, EndpointClient(&mut router));
16643            if matches!(connection.core.state, H2State::Header)
16644                && matches!(
16645                    connection.core.stream_table.expect_read(),
16646                    Some((H2StreamId::Zero, 9))
16647                )
16648                && !connection.core.control_tx.pending().is_empty()
16649            {
16650                break;
16651            }
16652            std::thread::yield_now();
16653        }
16654
16655        // Premise of the test: we really went through the refusal path, the
16656        // stream was refused rather than created, and the connection survived.
16657        assert_eq!(
16658            connection.core.control_tx.pending(),
16659            vec![(1, H2Error::RefusedStream)],
16660            "the drain gate must refuse stream 1 with RST_STREAM(REFUSED_STREAM)"
16661        );
16662        assert!(
16663            connection.core.stream_table.is_empty(),
16664            "a refused stream must not be created"
16665        );
16666        assert!(
16667            !matches!(connection.core.state, H2State::Error | H2State::GoAway),
16668            "refusing one stream must leave the connection usable: {:?}",
16669            connection.core.state
16670        );
16671
16672        // The peer's next block references the dynamic entry its encoder added
16673        // while encoding the refused request — a single indexed field.
16674        let next_block = peer_encoder.encode([(&b"x-sozu-probe"[..], &b"alpha"[..])]);
16675        assert_eq!(
16676            next_block.len(),
16677            1,
16678            "the peer must now reference its dynamic entry, not re-send a literal"
16679        );
16680
16681        let mut decoded = Vec::new();
16682        let status = connection
16683            .core
16684            .hpack
16685            .decoder_mut()
16686            .decode_with_cb(&next_block, |k, v| {
16687                decoded.push((k.into_owned(), v.into_owned()));
16688            });
16689
16690        assert!(
16691            status.is_ok(),
16692            "the connection decoder must still resolve the peer's dynamic table \
16693             after a refused stream, got {status:?}"
16694        );
16695        assert_eq!(
16696            decoded,
16697            vec![(b"x-sozu-probe".to_vec(), b"alpha".to_vec())],
16698            "the refused stream's field block must have been decoded into the \
16699             connection's HPACK context"
16700        );
16701    }
16702
16703    /// Serialise one frame header followed by `payload`.
16704    fn orphan_frame(
16705        frame_type: u8,
16706        flags: u8,
16707        stream_id: u32,
16708        len: u32,
16709        payload: &[u8],
16710    ) -> Vec<u8> {
16711        let mut frame = Vec::with_capacity(9 + payload.len());
16712        frame.extend_from_slice(&len.to_be_bytes()[1..]);
16713        frame.push(frame_type);
16714        frame.push(flags);
16715        frame.extend_from_slice(&stream_id.to_be_bytes());
16716        frame.extend_from_slice(payload);
16717        frame
16718    }
16719
16720    /// A PING the peer sends after the orphaned payload: once its ACK sits in
16721    /// `zero`, the connection has provably read past that payload.
16722    fn orphan_ping() -> Vec<u8> {
16723        orphan_frame(6, 0, 0, 8, b"pingpong")
16724    }
16725
16726    /// Open stream 1 with a request whose body follows, then send a DATA
16727    /// header announcing `payload_len` bytes and the first `sent` of them,
16728    /// and drive `readable()` until the connection waits on that payload.
16729    /// Returns what `expect_read` then names.
16730    fn open_stream_mid_data_payload(
16731        connection: &mut H2Shell<mio::net::TcpStream>,
16732        peer: &mut std::net::TcpStream,
16733        context: &mut Context<crate::protocol::mux::test_support::TestListener>,
16734        router: &mut Router,
16735        payload_len: u32,
16736        sent: usize,
16737    ) -> Option<(H2StreamId, usize)> {
16738        use std::io::Write;
16739
16740        connection.core.state = H2State::Header;
16741        connection
16742            .core
16743            .stream_table
16744            .set_expect_read(Some((H2StreamId::Zero, 9)));
16745        let block = crate::protocol::mux::hpack::Encoder::new().encode([
16746            (&b":method"[..], &b"POST"[..]),
16747            (&b":scheme"[..], &b"https"[..]),
16748            (&b":authority"[..], &b"example.com"[..]),
16749            (&b":path"[..], &b"/upload"[..]),
16750        ]);
16751        let mut bytes = orphan_frame(1, parser::FLAG_END_HEADERS, 1, block.len() as u32, &block);
16752        bytes.extend(orphan_frame(0, 0, 1, payload_len, &vec![b'x'; sent]));
16753        peer.write_all(&bytes)
16754            .expect("loopback write must complete");
16755        peer.flush().expect("loopback flush must complete");
16756        for _ in 0..64 {
16757            connection.core.readiness.event.insert(Ready::READABLE);
16758            connection.readable(context, EndpointClient(router));
16759            if let Some((H2StreamId::Other { .. }, remaining)) =
16760                connection.core.stream_table.expect_read()
16761                && (remaining == payload_len as usize - sent
16762                    || !connection.core.readiness.interest.is_readable())
16763            {
16764                break;
16765            }
16766            std::thread::yield_now();
16767        }
16768        connection.core.stream_table.expect_read()
16769    }
16770
16771    /// Drive `readable()` until `done` holds, re-inserting the READABLE event
16772    /// AND interest before every pass, so a lost edge-triggered event cannot
16773    /// be what stalls the connection. Returns whether `done` held.
16774    fn drive_reads_with_readiness_forced(
16775        connection: &mut H2Shell<mio::net::TcpStream>,
16776        context: &mut Context<crate::protocol::mux::test_support::TestListener>,
16777        router: &mut Router,
16778        done: impl Fn(&H2Shell<mio::net::TcpStream>) -> bool,
16779    ) -> bool {
16780        for _ in 0..256 {
16781            connection.core.readiness.event.insert(Ready::READABLE);
16782            connection.core.readiness.interest.insert(Ready::READABLE);
16783            connection.readable(context, EndpointClient(router));
16784            if done(connection) {
16785                return true;
16786            }
16787            std::thread::yield_now();
16788        }
16789        false
16790    }
16791
16792    /// Regression test for #1597: a stream reaped by the per-stream idle
16793    /// guard while it owns the DATA payload being read must not leave the
16794    /// connection unable to read.
16795    ///
16796    /// `H2StreamTable::remove` nulls an `expect_read` naming the removed
16797    /// stream. Before the fix nothing else changed, so the connection stayed
16798    /// in `H2State::Frame` with nothing to read, and every later `readable()`
16799    /// returned at the `expect_read() == None` early exit — the remaining
16800    /// payload, the PING behind it and every later frame were never read.
16801    ///
16802    /// TO SEE THIS RED: in [`ConnectionH2::remove_dead_stream`], delete the
16803    /// `skip_orphaned_data_payload` call. The test fails with `after the reap
16804    /// the connection must still read the PING behind the orphaned payload;
16805    /// state=Frame(FrameHeader { payload_len: 10, frame_type: Data, flags: 0,
16806    /// stream_id: 1 }) expect_read=None`. Verified 2026-09-26.
16807    #[test]
16808    fn reaping_a_stream_mid_data_payload_keeps_the_connection_reading() {
16809        use std::io::Write;
16810
16811        let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 20, 16_384)));
16812        let (mut connection, mut peer) = test_h2_connection(&pool, None);
16813        let mut context = test_context(&pool);
16814        let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
16815
16816        let expect = open_stream_mid_data_payload(
16817            &mut connection,
16818            &mut peer,
16819            &mut context,
16820            &mut router,
16821            10,
16822            4,
16823        );
16824        assert!(
16825            matches!(expect, Some((H2StreamId::Other { .. }, 6))),
16826            "premise: mid DATA payload with 6 bytes left, got {expect:?}"
16827        );
16828
16829        // The peer pauses past `stream_idle_timeout` (30 s in
16830        // `test_h2_connection`), then sends the rest and a PING.
16831        context.now += Duration::from_secs(31);
16832        let mut rest = b"efghij".to_vec();
16833        rest.extend(orphan_ping());
16834        peer.write_all(&rest).expect("loopback write must complete");
16835        peer.flush().expect("loopback flush must complete");
16836
16837        let read_on =
16838            drive_reads_with_readiness_forced(&mut connection, &mut context, &mut router, |c| {
16839                !c.core.output.is_empty()
16840            });
16841        assert!(
16842            read_on,
16843            "after the reap the connection must still read the PING behind the \
16844             orphaned payload; state={:?} expect_read={:?}",
16845            connection.core.state,
16846            connection.core.stream_table.expect_read()
16847        );
16848        assert!(
16849            connection.core.stream_table.is_empty(),
16850            "premise: the idle guard reaped stream 1"
16851        );
16852    }
16853
16854    /// #1597: the orphaned frame never reaches `handle_data_frame`, so its
16855    /// whole wire length must still be credited to the connection receive
16856    /// window (RFC 9113 §6.9), or every reap shrinks the peer's window.
16857    ///
16858    /// TO SEE THIS RED: in [`ConnectionH2::skip_orphaned_data_payload`],
16859    /// delete the `account_received_bytes` block. The test then fails with
16860    /// `the whole orphaned DATA frame must be credited to the connection
16861    /// window`. Verified 2026-09-26.
16862    #[test]
16863    fn a_skipped_orphaned_data_payload_is_credited_to_the_connection_window() {
16864        use std::io::Write;
16865
16866        let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 20, 16_384)));
16867        let (mut connection, mut peer) = test_h2_connection(&pool, None);
16868        let mut context = test_context(&pool);
16869        let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
16870
16871        let expect = open_stream_mid_data_payload(
16872            &mut connection,
16873            &mut peer,
16874            &mut context,
16875            &mut router,
16876            10,
16877            4,
16878        );
16879        assert!(matches!(expect, Some((H2StreamId::Other { .. }, 6))));
16880        context.now += Duration::from_secs(31);
16881        let mut rest = b"efghij".to_vec();
16882        rest.extend(orphan_ping());
16883        peer.write_all(&rest).expect("loopback write must complete");
16884        peer.flush().expect("loopback flush must complete");
16885        assert!(drive_reads_with_readiness_forced(
16886            &mut connection,
16887            &mut context,
16888            &mut router,
16889            |c| !c.core.output.is_empty(),
16890        ));
16891
16892        // A threshold of 1 hands back everything accumulated since the last
16893        // connection WINDOW_UPDATE: exactly the orphaned frame's length.
16894        assert_eq!(
16895            connection.core.flow_control.account_received_bytes(0, 1),
16896            Some(10),
16897            "the whole orphaned DATA frame must be credited to the connection window"
16898        );
16899    }
16900
16901    /// #1597: a remainder close to `max_frame_size` is skipped in pieces as
16902    /// small as the room in `zero` allows, never by waiting for room for all
16903    /// of it. With 4 KiB buffers a 16 KiB remainder can never fit at once.
16904    ///
16905    /// TO SEE THIS RED: in [`ConnectionH2::poll_read_target`], replace
16906    /// `amount.min(available)` by `amount`. The connection then parks for
16907    /// good on the full remainder and the test fails with `the orphaned
16908    /// remainder must be skipped in pieces`.
16909    #[test]
16910    fn a_remainder_near_max_frame_size_is_skipped_in_pieces() {
16911        use std::io::Write;
16912
16913        const PAYLOAD_LEN: u32 = 16_384;
16914
16915        let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 20, 4_096)));
16916        let (mut connection, mut peer) = test_h2_connection(&pool, None);
16917        let mut context = test_context(&pool);
16918        let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
16919
16920        let expect = open_stream_mid_data_payload(
16921            &mut connection,
16922            &mut peer,
16923            &mut context,
16924            &mut router,
16925            PAYLOAD_LEN,
16926            0,
16927        );
16928        assert!(
16929            matches!(expect, Some((H2StreamId::Other { .. }, n)) if n == PAYLOAD_LEN as usize),
16930            "premise: waiting on the whole DATA payload, got {expect:?}"
16931        );
16932        assert!(
16933            connection.core.zero.storage.capacity() < PAYLOAD_LEN as usize,
16934            "premise: the remainder cannot fit in `zero` at once"
16935        );
16936
16937        context.now += Duration::from_secs(31);
16938        let mut rest = vec![b'x'; PAYLOAD_LEN as usize];
16939        rest.extend(orphan_ping());
16940        peer.write_all(&rest).expect("loopback write must complete");
16941        peer.flush().expect("loopback flush must complete");
16942
16943        let read_on =
16944            drive_reads_with_readiness_forced(&mut connection, &mut context, &mut router, |c| {
16945                !c.core.output.is_empty()
16946            });
16947        assert!(
16948            read_on,
16949            "the orphaned remainder must be skipped in pieces; state={:?} expect_read={:?}",
16950            connection.core.state,
16951            connection.core.stream_table.expect_read()
16952        );
16953        assert!(
16954            matches!(connection.core.state, H2State::Header),
16955            "the connection must be back on frame headers, not in {:?}",
16956            connection.core.state
16957        );
16958        assert_eq!(
16959            connection.core.flow_control.account_received_bytes(0, 1),
16960            Some(PAYLOAD_LEN),
16961            "the whole orphaned DATA frame must be credited to the connection window"
16962        );
16963    }
16964
16965    /// #1597, parked form: a stream parked on buffer pressure (READABLE
16966    /// interest removed until `try_resume_reading` sees room) and then
16967    /// reaped by `Mux::timeout`'s `cancel_timed_out_streams` must resume
16968    /// reading. `try_resume_reading` only resumes a stream that still
16969    /// exists, so nothing else restores the interest.
16970    ///
16971    /// Unlike the other #1597 tests, this one never re-inserts READABLE
16972    /// interest itself: it calls `readable()` only when the mux would.
16973    ///
16974    /// TO SEE THIS RED: in [`ConnectionH2::skip_orphaned_data_payload`],
16975    /// delete `self.readiness.interest.insert(Ready::READABLE);`. The test
16976    /// fails with `a reaped parked stream must not leave the connection
16977    /// unable to read`.
16978    #[test]
16979    fn reaping_a_stream_parked_on_buffer_pressure_resumes_reading() {
16980        use std::io::Write;
16981
16982        const PAYLOAD_LEN: u32 = 16_384;
16983
16984        let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 20, 4_096)));
16985        let (mut connection, mut peer) = test_h2_connection(&pool, None);
16986        let mut context = test_context(&pool);
16987        let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
16988
16989        let expect = open_stream_mid_data_payload(
16990            &mut connection,
16991            &mut peer,
16992            &mut context,
16993            &mut router,
16994            PAYLOAD_LEN,
16995            0,
16996        );
16997        assert!(
16998            matches!(expect, Some((H2StreamId::Other { .. }, n)) if n == PAYLOAD_LEN as usize),
16999            "premise: waiting on the whole DATA payload, got {expect:?}"
17000        );
17001        // The next pass finds no room for the frame in the stream buffer.
17002        connection.readable(&mut context, EndpointClient(&mut router));
17003        assert!(
17004            !connection.core.readiness.interest.is_readable(),
17005            "premise: the stream buffer cannot take the frame, so the read parked"
17006        );
17007
17008        let mut rest = vec![b'x'; PAYLOAD_LEN as usize];
17009        rest.extend(orphan_ping());
17010        peer.write_all(&rest).expect("loopback write must complete");
17011        peer.flush().expect("loopback flush must complete");
17012
17013        // `Mux::timeout` reaps the parked stream.
17014        context.now += Duration::from_secs(31);
17015        connection.cancel_timed_out_streams(&mut context, &mut EndpointClient(&mut router));
17016        assert!(
17017            connection.core.stream_table.is_empty(),
17018            "premise: the idle guard reaped stream 1"
17019        );
17020
17021        let mut read_on = false;
17022        for _ in 0..256 {
17023            connection.core.readiness.event.insert(Ready::READABLE);
17024            if connection.core.readiness.filter_interest().is_readable() {
17025                connection.readable(&mut context, EndpointClient(&mut router));
17026            }
17027            if !connection.core.output.is_empty() {
17028                read_on = true;
17029                break;
17030            }
17031            std::thread::yield_now();
17032        }
17033        assert!(
17034            read_on,
17035            "a reaped parked stream must not leave the connection unable to read; \
17036             state={:?} expect_read={:?} interest={:?}",
17037            connection.core.state,
17038            connection.core.stream_table.expect_read(),
17039            connection.core.readiness.interest
17040        );
17041    }
17042
17043    /// #1597, backend form: `end_stream` on a `Position::Client` connection
17044    /// (the client gave up, or the backend timeout fired) while the backend
17045    /// is mid-way through a response DATA frame. The RST_STREAM(CANCEL)
17046    /// `end_stream` queues must reach the backend, ahead of the PING ACK the
17047    /// connection queues once it reads on. Before #1604 the RST was written
17048    /// into `zero`, which `handle_header_state` parses from its first byte,
17049    /// so an unflushed RST there was read back as if the backend had sent it,
17050    /// then cleared, and never sent; the output queue shares nothing with the
17051    /// read side any more.
17052    ///
17053    /// The loop drives the connection the way `Mux` does: `writable()` and
17054    /// `readable()` only when the filtered readiness allows them.
17055    ///
17056    /// TO SEE THIS RED: in the `Position::Client` arm of
17057    /// [`ConnectionH2::end_stream`], make the RST's `if` condition start with
17058    /// `false &&`, so the RST is never queued. The test then fails with `the
17059    /// backend must receive exactly RST_STREAM(CANCEL) for stream 1, then the
17060    /// PING ACK`, `left` being the ACK alone. Verified 2026-09-27.
17061    #[test]
17062    fn end_stream_on_a_backend_mid_data_payload_sends_its_rst_before_reading_on() {
17063        use std::io::{Read, Write};
17064
17065        let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 20, 16_384)));
17066        let (socket, mut peer) = connected_socket();
17067        peer.set_nonblocking(true)
17068            .expect("the loopback peer must switch to non-blocking");
17069        let backend = Rc::new(RefCell::new(crate::backends::Backend::new(
17070            "orphan-backend",
17071            "127.0.0.1:2".parse().expect("backend address must parse"),
17072            None,
17073            None,
17074            None,
17075        )));
17076        let mut registry = crate::protocol::mux::BackendRegistry::default();
17077        let mut connection = H2Shell::new(
17078            Ulid::generate(),
17079            socket,
17080            Position::Client(
17081                "orphan-cluster".into(),
17082                registry.id_for(&backend),
17083                BackendStatus::Connected,
17084            ),
17085            &mut PoolBufferSource::new(Rc::downgrade(&pool)),
17086            H2FloodConfig::default(),
17087            H2ConnectionConfig::default(),
17088            Duration::from_secs(30),
17089            None,
17090            Duration::from_secs(30),
17091            Some((H2StreamId::Zero, 9)),
17092            Ready::READABLE | Ready::HUP | Ready::ERROR,
17093        )
17094        .expect("a pool with free buffers must yield an H2 connection");
17095        connection.core.state = H2State::Header;
17096        let mut context = test_context(&pool);
17097        let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
17098        let gid = context
17099            .create_stream(Ulid::generate(), 1 << 16)
17100            .expect("test context must create a stream");
17101        context.streams[gid].state = StreamState::Linked(mio::Token(1));
17102        // The backend is answering, so the request's HEADERS reached it: only
17103        // a stream opened on the wire is reset (sozu-proxy/sozu#1631).
17104        context.streams[gid].front.consumed = true;
17105        connection
17106            .core
17107            .stream_table
17108            .register(1, gid, connection.core.now);
17109
17110        // The backend starts a 10-byte response DATA frame and sends 4 bytes.
17111        peer.write_all(&orphan_frame(0, 0, 1, 10, b"abcd"))
17112            .expect("loopback write must complete");
17113        for _ in 0..64 {
17114            connection.core.readiness.event.insert(Ready::READABLE);
17115            connection.readable(&mut context, EndpointClient(&mut router));
17116            if matches!(
17117                connection.core.stream_table.expect_read(),
17118                Some((H2StreamId::Other { .. }, 6))
17119            ) {
17120                break;
17121            }
17122            std::thread::yield_now();
17123        }
17124        assert!(
17125            matches!(
17126                connection.core.stream_table.expect_read(),
17127                Some((H2StreamId::Other { .. }, 6))
17128            ),
17129            "premise: mid response DATA payload with 6 bytes left, got {:?}",
17130            connection.core.stream_table.expect_read()
17131        );
17132
17133        connection.end_stream(gid, &mut context);
17134
17135        // The backend, not yet aware of the RST, finishes the frame and pings.
17136        let mut rest = b"efghij".to_vec();
17137        rest.extend(orphan_ping());
17138        peer.write_all(&rest).expect("loopback write must complete");
17139
17140        let mut received = Vec::new();
17141        let expected_len = 13 + 17;
17142        for _ in 0..256 {
17143            connection
17144                .core
17145                .readiness
17146                .event
17147                .insert(Ready::READABLE | Ready::WRITABLE);
17148            if connection.core.readiness.filter_interest().is_writable() {
17149                connection.writable(&mut context, EndpointClient(&mut router));
17150            }
17151            if connection.core.readiness.filter_interest().is_readable() {
17152                connection.readable(&mut context, EndpointClient(&mut router));
17153            }
17154            let mut buf = [0u8; 256];
17155            if let Ok(n) = peer.read(&mut buf) {
17156                received.extend_from_slice(&buf[..n]);
17157            }
17158            if received.len() >= expected_len
17159                || matches!(connection.core.state, H2State::GoAway | H2State::Error)
17160            {
17161                break;
17162            }
17163            std::thread::yield_now();
17164        }
17165
17166        let mut expected = orphan_frame(3, 0, 1, 4, &(H2Error::Cancel as u32).to_be_bytes());
17167        expected.extend(orphan_frame(6, parser::FLAG_ACK, 0, 8, b"pingpong"));
17168        assert_eq!(
17169            received,
17170            expected,
17171            "the backend must receive exactly RST_STREAM(CANCEL) for stream 1, then the \
17172             PING ACK; state={:?} expect_read={:?}",
17173            connection.core.state,
17174            connection.core.stream_table.expect_read()
17175        );
17176    }
17177
17178    /// What [`backend_with_a_linked_stream`] builds. Callers bind `_pool`
17179    /// first so it is dropped last, after everything holding its buffers.
17180    struct LinkedBackend {
17181        connection: H2Shell<mio::net::TcpStream>,
17182        /// The backend's end of the socket.
17183        peer: std::net::TcpStream,
17184        context: Context<TestListener>,
17185        router: Router,
17186        /// The linked stream's global id.
17187        gid: GlobalStreamId,
17188        _pool: Rc<RefCell<Pool>>,
17189    }
17190
17191    /// A backend (`Position::Client`) connection in `state` over a loopback
17192    /// socket, with `interest`, and one stream linked to it by
17193    /// [`H2Shell::start_stream`]: stream 1, whose HEADERS no write pass has
17194    /// sent.
17195    fn backend_with_a_linked_stream(
17196        state: H2State,
17197        status: BackendStatus,
17198        interest: Ready,
17199    ) -> LinkedBackend {
17200        let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 20, 16_384)));
17201        let (socket, peer) = connected_socket();
17202        let backend = Rc::new(RefCell::new(crate::backends::Backend::new(
17203            "idle-backend",
17204            "127.0.0.1:2".parse().expect("backend address must parse"),
17205            None,
17206            None,
17207            None,
17208        )));
17209        let mut registry = crate::protocol::mux::BackendRegistry::default();
17210        let expect_read = match state {
17211            H2State::ClientPreface | H2State::ClientSettings => None,
17212            _ => Some((H2StreamId::Zero, 9)),
17213        };
17214        let mut connection = H2Shell::new(
17215            Ulid::generate(),
17216            socket,
17217            Position::Client("idle-cluster".into(), registry.id_for(&backend), status),
17218            &mut PoolBufferSource::new(Rc::downgrade(&pool)),
17219            H2FloodConfig::default(),
17220            H2ConnectionConfig::default(),
17221            Duration::from_secs(30),
17222            None,
17223            Duration::from_secs(30),
17224            expect_read,
17225            interest,
17226        )
17227        .expect("a pool with free buffers must yield an H2 connection");
17228        connection.core.state = state;
17229        let mut context = test_context(&pool);
17230        let router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
17231        let gid = context
17232            .create_stream(Ulid::generate(), 1 << 16)
17233            .expect("test context must create a stream");
17234        assert!(
17235            connection.start_stream(gid, &mut context),
17236            "premise: the backend connection accepts the stream"
17237        );
17238        assert_eq!(
17239            connection.core.stream_table.get(1),
17240            Some(gid),
17241            "premise: the stream got wire id 1"
17242        );
17243        LinkedBackend {
17244            connection,
17245            peer,
17246            context,
17247            router,
17248            gid,
17249            _pool: pool,
17250        }
17251    }
17252
17253    /// Drive a backend connection the way `Mux` does — `writable()` and
17254    /// `readable()` only when the filtered readiness allows them — and answer
17255    /// every byte its peer end received.
17256    fn drive_and_read_backend(
17257        connection: &mut H2Shell<mio::net::TcpStream>,
17258        peer: &mut std::net::TcpStream,
17259        context: &mut Context<TestListener>,
17260        router: &mut Router,
17261    ) -> Vec<u8> {
17262        use std::io::Read;
17263
17264        let mut received = Vec::new();
17265        for _ in 0..64 {
17266            connection
17267                .core
17268                .readiness
17269                .event
17270                .insert(Ready::READABLE | Ready::WRITABLE);
17271            if connection.core.readiness.filter_interest().is_writable() {
17272                connection.writable(context, EndpointClient(router));
17273            }
17274            if connection.core.readiness.filter_interest().is_readable() {
17275                connection.readable(context, EndpointClient(router));
17276            }
17277            let mut buf = [0u8; 256];
17278            if let Ok(n) = peer.read(&mut buf) {
17279                received.extend_from_slice(&buf[..n]);
17280            }
17281            std::thread::yield_now();
17282        }
17283        received
17284    }
17285
17286    /// Queue a GET request on `gid`'s request side, closed with END_STREAM,
17287    /// the way a relayed request reaches a backend's write pass.
17288    fn queue_request(context: &mut Context<TestListener>, gid: GlobalStreamId) {
17289        let kawa = &mut context.streams[gid].front;
17290        kawa.detached.status_line = kawa::StatusLine::Request {
17291            version: kawa::Version::V20,
17292            method: kawa::Store::Static(b"GET"),
17293            uri: kawa::Store::Static(b"/"),
17294            authority: kawa::Store::Static(b"example.com"),
17295            path: kawa::Store::Static(b"/"),
17296        };
17297        kawa.push_block(kawa::Block::StatusLine);
17298        kawa.push_block(kawa::Block::Header(kawa::Pair {
17299            key: kawa::Store::Static(b"x-client"),
17300            val: kawa::Store::Static(b"alice"),
17301        }));
17302        kawa.push_block(kawa::Block::Flags(kawa::Flags {
17303            end_body: false,
17304            end_chunk: false,
17305            end_header: true,
17306            end_stream: true,
17307        }));
17308        kawa.parsing_phase = kawa::ParsingPhase::Terminated;
17309    }
17310
17311    /// Frame-level pin of `handle_data_frame`'s bodiless branches: a backend
17312    /// answers `method` with the HEADERS block `head` (no END_STREAM, so the
17313    /// stream stays linked, RFC 9113 §8.1), then DATA carrying `hello`
17314    /// flagged END_STREAM. Returns the frames the backend reads after the
17315    /// DATA, whether the payload was queued on the response, whether the
17316    /// stream is still tracked, and how many bytes the DATA frame left in the
17317    /// response buffer.
17318    fn bodiless_response_with_data(
17319        method: crate::protocol::kawa_h1::parser::Method,
17320        head: &[u8],
17321    ) -> (Vec<PeerFrame>, bool, bool, usize) {
17322        use std::io::Write;
17323
17324        let LinkedBackend {
17325            _pool,
17326            mut connection,
17327            mut peer,
17328            mut context,
17329            mut router,
17330            gid,
17331        } = backend_with_a_linked_stream(
17332            H2State::Header,
17333            BackendStatus::Connected,
17334            Ready::READABLE | Ready::HUP | Ready::ERROR,
17335        );
17336        queue_request(&mut context, gid);
17337        context.streams[gid].context.method = Some(method);
17338        // Open on the client side, as a stream the router linked; `Link`
17339        // carries no frontend token for `reset_stream` to end.
17340        context.streams[gid].state = StreamState::Link;
17341        let request = drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
17342        assert!(
17343            peer_frames(&request)
17344                .expect("whole frames")
17345                .iter()
17346                .any(|(kind, _, id, _)| *kind == 1 && *id == 1),
17347            "premise: the request went out on stream 1, got {request:?}"
17348        );
17349
17350        peer.write_all(&orphan_frame(1, 0x4, 1, head.len() as u32, head))
17351            .expect("loopback write must complete");
17352        drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
17353        let end_before = context.streams[gid].back.storage.end;
17354        peer.write_all(&orphan_frame(0, 0x1, 1, 5, b"hello"))
17355            .expect("loopback write must complete");
17356        let after = drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
17357        let after = peer_frames(&after).expect("whole frames");
17358        assert!(
17359            matches!(connection.core.state, H2State::Header),
17360            "the shared connection stays up, got {:?}",
17361            connection.core.state
17362        );
17363        let back = &context.streams[gid].back;
17364        let buffer = back.storage.buffer();
17365        let queued = back.blocks.iter().any(|block| {
17366            matches!(block, kawa::Block::Chunk(kawa::Chunk { data })
17367                if !data.data(buffer).is_empty())
17368        });
17369        let tracked = connection.core.stream_table.get(1).is_some();
17370        let retained = back.storage.end.saturating_sub(end_before);
17371        (after, queued, tracked, retained)
17372    }
17373
17374    /// RFC 9110 §15.3.5, §15.4.5, RFC 9113 §8.1.1: DATA carrying a payload
17375    /// on a 204 still linked to its backend stream resets that stream with
17376    /// PROTOCOL_ERROR before the payload is queued, without a GOAWAY. RFC 9110
17377    /// §9.3.2: the payload of a response to HEAD is dropped from the stream
17378    /// buffer and never queued, and its END_STREAM still ends the response,
17379    /// with no frame sent. A 200 to GET forwards the payload.
17380    ///
17381    /// TO SEE THIS RED: make `ConnectionH2::backend_response_content` return
17382    /// `BackendResponseContent::Forwarded` for every response, or keep the
17383    /// discarded payload behind `storage.head` in `handle_data_frame`.
17384    #[test]
17385    fn a_204_response_carrying_data_resets_its_backend_stream_and_a_head_response_discards_it() {
17386        use crate::protocol::kawa_h1::parser::Method;
17387
17388        // :status 204 (static index 9).
17389        let (frames, queued, tracked, _) = bodiless_response_with_data(Method::Get, &[0x89]);
17390        assert_eq!(
17391            frames,
17392            vec![(3, 0, 1, vec![0, 0, 0, 1])],
17393            "a 204 carrying DATA gets RST_STREAM(PROTOCOL_ERROR) alone"
17394        );
17395        assert!(!queued, "the payload of a 204 is never queued");
17396        assert!(!tracked, "the reset stream is retired");
17397
17398        // :status 200 (static index 8), to HEAD.
17399        let (frames, queued, _, retained) = bodiless_response_with_data(Method::Head, &[0x88]);
17400        assert!(
17401            !frames
17402                .iter()
17403                .any(|(kind, _, _, _)| *kind == 3 || *kind == 7),
17404            "a response to HEAD carrying DATA is not reset, got {frames:?}"
17405        );
17406        assert!(!queued, "the payload of a response to HEAD is never queued");
17407        assert_eq!(
17408            retained, 0,
17409            "the payload of a response to HEAD leaves no byte in the buffer"
17410        );
17411
17412        let (frames, queued, _, retained) = bodiless_response_with_data(Method::Get, &[0x88]);
17413        assert!(
17414            !frames
17415                .iter()
17416                .any(|(kind, _, _, _)| *kind == 3 || *kind == 7),
17417            "premise: a 200 to GET is not reset, got {frames:?}"
17418        );
17419        assert!(queued, "premise: the payload of a 200 to GET is queued");
17420        assert_eq!(
17421            retained, 5,
17422            "premise: a forwarded payload stays in the buffer"
17423        );
17424    }
17425
17426    /// RFC 9113 §5.1: after the PROTOCOL_ERROR reset of a 204 carrying DATA,
17427    /// the trailer block the backend had already sent on that stream is
17428    /// ignored, in one HEADERS frame or split over a CONTINUATION frame
17429    /// (RFC 9113 §6.10). The shared backend connection gets no GOAWAY and no
17430    /// further RST_STREAM, and stays up.
17431    ///
17432    /// TO SEE THIS RED: in the `H2State::Discard` arm of
17433    /// `ConnectionH2::handle_read` (`lib/src/protocol/mux/h2.rs`), drop the
17434    /// `pending_discarded_block` store: the CONTINUATION is taken for a
17435    /// standalone frame and answered GOAWAY(PROTOCOL_ERROR):
17436    /// `split=true: ... got [(7, 0, 0, [0, 0, 0, 0, 0, 0, 0, 1])]`, verified
17437    /// 2026-10-02. The split row is also red on `2ce07244`
17438    /// (sozu-proxy/sozu#1775 before sozu-proxy/sozu#1784 was merged), where
17439    /// the unsplit row already passes.
17440    #[test]
17441    fn a_trailer_after_the_reset_of_a_204_carrying_data_is_ignored() {
17442        use std::io::Write;
17443
17444        for split in [false, true] {
17445            let LinkedBackend {
17446                _pool,
17447                mut connection,
17448                mut peer,
17449                mut context,
17450                mut router,
17451                gid,
17452            } = backend_with_a_linked_stream(
17453                H2State::Header,
17454                BackendStatus::Connected,
17455                Ready::READABLE | Ready::HUP | Ready::ERROR,
17456            );
17457            queue_request(&mut context, gid);
17458            context.streams[gid].context.method =
17459                Some(crate::protocol::kawa_h1::parser::Method::Get);
17460            context.streams[gid].state = StreamState::Link;
17461            drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
17462
17463            // :status 204 (static index 9), without END_STREAM, then DATA
17464            // without END_STREAM: the backend means to end with a trailer.
17465            peer.write_all(&orphan_frame(1, 0x4, 1, 1, &[0x89]))
17466                .expect("loopback write must complete");
17467            drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
17468            peer.write_all(&orphan_frame(0, 0x0, 1, 5, b"hello"))
17469                .expect("loopback write must complete");
17470            let reset =
17471                drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
17472            let reset = peer_frames(&reset).expect("whole frames");
17473            assert_eq!(
17474                reset,
17475                vec![(3, 0, 1, vec![0, 0, 0, 1])],
17476                "split={split}: premise: a 204 carrying DATA gets RST_STREAM(PROTOCOL_ERROR) alone"
17477            );
17478
17479            // The trailer block `x-trailer: one` (literal without indexing),
17480            // ending the stream.
17481            let mut trailer = vec![0x00, 9];
17482            trailer.extend_from_slice(b"x-trailer");
17483            trailer.extend_from_slice(&[3]);
17484            trailer.extend_from_slice(b"one");
17485            let wire = if split {
17486                let (first, rest) = trailer.split_at(4);
17487                let mut wire = orphan_frame(1, 0x1, 1, first.len() as u32, first);
17488                wire.extend(orphan_frame(9, 0x4, 1, rest.len() as u32, rest));
17489                wire
17490            } else {
17491                orphan_frame(1, 0x4 | 0x1, 1, trailer.len() as u32, &trailer)
17492            };
17493            peer.write_all(&wire).expect("loopback write must complete");
17494            let after =
17495                drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
17496            let after = peer_frames(&after).expect("whole frames");
17497            assert!(
17498                !after.iter().any(|(kind, _, _, _)| *kind == 7 || *kind == 3),
17499                "split={split}: the trailer on the reset stream is ignored: no GOAWAY, no \
17500                 RST_STREAM, got {after:?}"
17501            );
17502            assert!(
17503                matches!(connection.core.state, H2State::Header),
17504                "split={split}: the shared connection stays up, got {:?}",
17505                connection.core.state
17506            );
17507        }
17508    }
17509
17510    /// sozu-proxy/sozu#1631, `Header` row: a backend stream ended before any
17511    /// write pass sent its HEADERS is idle on the backend, and a RST_STREAM
17512    /// for it is a connection error there (RFC 9113 §5.1, §6.4) that takes
17513    /// down every stream of the shared connection. It must be forgotten: no
17514    /// frame at all.
17515    ///
17516    /// TO SEE THIS RED: in the `Position::Client` arm of
17517    /// [`ConnectionH2::end_stream`], replace `opened_on_the_wire` in the RST
17518    /// condition with `true`. The backend then reads `RST_STREAM(1, CANCEL)`:
17519    /// `left: [0, 0, 4, 3, 0, 0, 0, 0, 1, 0, 0, 0, 8], right: []`. Red on
17520    /// `d5161919`, verified 2026-09-28.
17521    #[test]
17522    fn a_backend_stream_ended_before_its_headers_left_is_not_reset() {
17523        let LinkedBackend {
17524            _pool,
17525            mut connection,
17526            mut peer,
17527            mut context,
17528            mut router,
17529            gid,
17530        } = backend_with_a_linked_stream(
17531            H2State::Header,
17532            BackendStatus::Connected,
17533            Ready::READABLE | Ready::HUP | Ready::ERROR,
17534        );
17535
17536        connection.end_stream(gid, &mut context);
17537        let received =
17538            drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
17539
17540        assert_eq!(
17541            received,
17542            Vec::<u8>::new(),
17543            "the backend must receive nothing for a stream it never saw"
17544        );
17545        assert!(
17546            connection.core.stream_table.is_empty()
17547                && !connection.core.stream_table.rst_sent_contains(1),
17548            "the stream is retired without a reset"
17549        );
17550        assert!(
17551            matches!(connection.core.state, H2State::Header),
17552            "the shared connection stays up, got {:?}",
17553            connection.core.state
17554        );
17555    }
17556
17557    /// sozu-proxy/sozu#1631, `ClientPreface` row: the backend connection's
17558    /// TCP connect is still in progress when the stream ends. The preface
17559    /// must be the first bytes the backend reads (RFC 9113 §3.4), with no
17560    /// frame for the stream anywhere.
17561    ///
17562    /// TO SEE THIS RED: same edit as
17563    /// `a_backend_stream_ended_before_its_headers_left_is_not_reset`. The
17564    /// backend then reads the RST before `PRI * HTTP/2.0`: `the connection
17565    /// preface must come first`. Red on `d5161919`, verified 2026-09-28.
17566    #[test]
17567    fn a_backend_stream_ended_before_the_preface_leaves_the_preface_first() {
17568        let LinkedBackend {
17569            _pool,
17570            mut connection,
17571            mut peer,
17572            mut context,
17573            mut router,
17574            gid,
17575        } = backend_with_a_linked_stream(
17576            H2State::ClientPreface,
17577            BackendStatus::Connecting(Instant::now()),
17578            Ready::WRITABLE | Ready::HUP | Ready::ERROR,
17579        );
17580
17581        connection.end_stream(gid, &mut context);
17582        let received =
17583            drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
17584
17585        let preface = serializer::H2_PRI.as_bytes();
17586        assert!(
17587            received.starts_with(preface),
17588            "the connection preface must come first, got {received:?}"
17589        );
17590        let frames =
17591            peer_frames(&received[preface.len()..]).expect("whole frames after the preface");
17592        assert!(
17593            frames.iter().all(|(kind, _, id, _)| *id == 0 && *kind != 3),
17594            "only connection-level frames may follow, got {frames:?}"
17595        );
17596    }
17597
17598    /// sozu-proxy/sozu#1631, `ServerSettings` row: the preface is out and the
17599    /// server's SETTINGS are not read yet. Neither ending a stream nor
17600    /// linking one may kill the connection; `start_stream` arms a write, as
17601    /// it must for `Router::plan_connect`, whose pool scan links onto a
17602    /// `Connected` connection whatever its handshake state.
17603    ///
17604    /// TO SEE THIS RED: in [`ConnectionH2::dispatch_writable_state`], move
17605    /// `(H2State::ServerSettings, Position::Client(..))` back into the
17606    /// `Unexpected combination` arm. The connection then force-disconnects:
17607    /// `the shared connection must survive the handshake, got Error`. Red on
17608    /// `d5161919`, verified 2026-09-28.
17609    #[test]
17610    fn a_backend_stream_ended_during_the_handshake_keeps_the_connection() {
17611        let LinkedBackend {
17612            _pool,
17613            mut connection,
17614            mut peer,
17615            mut context,
17616            mut router,
17617            gid,
17618        } = backend_with_a_linked_stream(
17619            H2State::ServerSettings,
17620            BackendStatus::Connected,
17621            Ready::READABLE | Ready::HUP | Ready::ERROR,
17622        );
17623
17624        connection.end_stream(gid, &mut context);
17625        let received =
17626            drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
17627
17628        assert!(
17629            matches!(connection.core.state, H2State::ServerSettings),
17630            "the shared connection must survive the handshake, got {:?}",
17631            connection.core.state
17632        );
17633        assert_eq!(
17634            received,
17635            Vec::<u8>::new(),
17636            "the backend must receive nothing for a stream it never saw"
17637        );
17638    }
17639
17640    /// A backend may answer early, before the whole request reached it
17641    /// (RFC 9113 §8.1). When the stream then ends with the request received
17642    /// whole from the client but its tail still unsent toward the backend,
17643    /// the backend never sees END_STREAM: forgetting the stream without a
17644    /// reset leaves it open (half-closed) on the backend, holding one of the
17645    /// backend's MAX_CONCURRENT_STREAMS slots for this connection's lifetime
17646    /// (RFC 9113 §5.1.2). The stream must be cancelled, exactly once.
17647    ///
17648    /// TO SEE THIS RED: in the `Position::Client` arm of
17649    /// [`ConnectionH2::end_stream`], drop `&& stream.front.is_completed()`
17650    /// from `fully_completed`. The backend then reads no RST_STREAM: `the
17651    /// backend must read RST_STREAM(1, CANCEL) once`, `right` holding that
17652    /// frame and `left` nothing.
17653    #[test]
17654    fn a_backend_stream_answered_before_its_request_was_written_is_cancelled() {
17655        let LinkedBackend {
17656            _pool,
17657            mut connection,
17658            mut peer,
17659            mut context,
17660            mut router,
17661            gid,
17662        } = backend_with_a_linked_stream(
17663            H2State::Header,
17664            BackendStatus::Connected,
17665            Ready::READABLE | Ready::HUP | Ready::ERROR,
17666        );
17667        // A request received whole from the client: its HEADERS, then a
17668        // 4-byte body closed with END_STREAM.
17669        let kawa = &mut context.streams[gid].front;
17670        kawa.detached.status_line = kawa::StatusLine::Request {
17671            version: kawa::Version::V20,
17672            method: kawa::Store::Static(b"POST"),
17673            uri: kawa::Store::Static(b"/"),
17674            authority: kawa::Store::Static(b"example.com"),
17675            path: kawa::Store::Static(b"/"),
17676        };
17677        kawa.push_block(kawa::Block::StatusLine);
17678        kawa.push_block(kawa::Block::Header(kawa::Pair {
17679            key: kawa::Store::Static(b"content-length"),
17680            val: kawa::Store::Static(b"4"),
17681        }));
17682        kawa.push_block(kawa::Block::Flags(kawa::Flags {
17683            end_body: false,
17684            end_chunk: false,
17685            end_header: true,
17686            end_stream: false,
17687        }));
17688        kawa.push_block(kawa::Block::Chunk(kawa::Chunk {
17689            data: kawa::Store::Static(b"body"),
17690        }));
17691        kawa.push_block(kawa::Block::Flags(kawa::Flags {
17692            end_body: true,
17693            end_chunk: false,
17694            end_header: false,
17695            end_stream: true,
17696        }));
17697        kawa.body_size = kawa::BodySize::Length(4);
17698        kawa.parsing_phase = kawa::ParsingPhase::Terminated;
17699        // The backend grants the stream no send window: its HEADERS leave,
17700        // its body waits.
17701        context.streams[gid].back_window = 0;
17702        let sent = drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
17703        let frames = peer_frames(&sent).expect("whole frames");
17704        assert!(
17705            frames
17706                .iter()
17707                .any(|(kind, flags, id, _)| *kind == 1 && *id == 1 && flags & 0x1 == 0)
17708                && !frames.iter().any(|(kind, _, _, _)| *kind == 0),
17709            "premise: the HEADERS left without END_STREAM and no DATA, got {frames:?}"
17710        );
17711        let stream = &context.streams[gid];
17712        assert!(
17713            stream.front.consumed && stream.front.is_terminated() && !stream.front.is_completed(),
17714            "premise: the request was received whole but not written whole"
17715        );
17716        // The backend's complete response arrived: its END_STREAM was read.
17717        context.streams[gid].back_received_end_of_stream = true;
17718
17719        connection.end_stream(gid, &mut context);
17720        // Ending it again must not reset it a second time.
17721        connection.end_stream(gid, &mut context);
17722        let received =
17723            drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
17724
17725        assert_eq!(
17726            received,
17727            orphan_frame(3, 0, 1, 4, &(H2Error::Cancel as u32).to_be_bytes()),
17728            "the backend must read RST_STREAM(1, CANCEL) once"
17729        );
17730        assert!(
17731            connection.core.stream_table.is_empty(),
17732            "the stream is retired from the backend connection"
17733        );
17734        assert!(
17735            matches!(connection.core.state, H2State::Header),
17736            "the shared connection stays up, got {:?}",
17737            connection.core.state
17738        );
17739    }
17740
17741    /// The liveness half of the `ServerSettings` fix: the write readiness
17742    /// withdrawn while the server's SETTINGS are awaited comes back with
17743    /// them, and a request linked during the handshake goes out after the
17744    /// SETTINGS ACK.
17745    ///
17746    /// TO SEE THIS RED: in [`ConnectionH2::dispatch_writable_state`], move
17747    /// `(H2State::ServerSettings, Position::Client(..))` back into the
17748    /// `Unexpected combination` arm: the connection force-disconnects before
17749    /// the SETTINGS arrive, `premise: nothing is written before the server
17750    /// SETTINGS, got [] in Error`. Red on `d5161919`, verified 2026-09-28.
17751    #[test]
17752    fn a_request_linked_during_the_handshake_goes_out_after_the_settings() {
17753        use std::io::Write;
17754
17755        let LinkedBackend {
17756            _pool,
17757            mut connection,
17758            mut peer,
17759            mut context,
17760            mut router,
17761            gid,
17762        } = backend_with_a_linked_stream(
17763            H2State::ServerSettings,
17764            BackendStatus::Connected,
17765            Ready::READABLE | Ready::HUP | Ready::ERROR,
17766        );
17767        queue_request(&mut context, gid);
17768        let before = drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
17769        assert!(
17770            before.is_empty() && matches!(connection.core.state, H2State::ServerSettings),
17771            "premise: nothing is written before the server SETTINGS, got {before:?} in {:?}",
17772            connection.core.state
17773        );
17774
17775        peer.write_all(&orphan_frame(4, 0, 0, 0, b""))
17776            .expect("loopback write must complete");
17777        let after = drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
17778
17779        let frames = peer_frames(&after).expect("whole frames");
17780        assert!(
17781            frames
17782                .iter()
17783                .any(|(kind, flags, id, _)| *kind == 4 && *flags == parser::FLAG_ACK && *id == 0),
17784            "the server SETTINGS are acknowledged, got {frames:?}"
17785        );
17786        assert!(
17787            frames.iter().any(|(kind, _, id, _)| *kind == 1 && *id == 1),
17788            "the request linked during the handshake must go out, got {frames:?}"
17789        );
17790    }
17791
17792    /// A backend stream 1 whose response is framed by `content-length: 3`,
17793    /// its request sent and its response carrying 5 bytes of DATA, which
17794    /// makes Sōzu reset it with PROTOCOL_ERROR (RFC 9113 §8.1.1). Returns the
17795    /// fixture, ready for the frames the backend sends after the reset.
17796    fn backend_stream_reset_for_its_content_length() -> LinkedBackend {
17797        use std::io::Write;
17798
17799        let mut linked = backend_with_a_linked_stream(
17800            H2State::Header,
17801            BackendStatus::Connected,
17802            Ready::READABLE | Ready::HUP | Ready::ERROR,
17803        );
17804        let LinkedBackend {
17805            connection,
17806            peer,
17807            context,
17808            router,
17809            gid,
17810            ..
17811        } = &mut linked;
17812        queue_request(context, *gid);
17813        // Open on the client side, as a stream the router linked; `Link`
17814        // carries no frontend token for `reset_stream` to end.
17815        context.streams[*gid].state = StreamState::Link;
17816        let request = drive_and_read_backend(connection, peer, context, router);
17817        assert!(
17818            peer_frames(&request)
17819                .expect("whole frames")
17820                .iter()
17821                .any(|(kind, _, id, _)| *kind == 1 && *id == 1),
17822            "premise: the request went out on stream 1, got {request:?}"
17823        );
17824        // :status 200 (static index 8), then `content-length: 3` as a
17825        // literal without indexing.
17826        let mut head = vec![0x88, 0x00, 14];
17827        head.extend_from_slice(b"content-length");
17828        head.extend_from_slice(&[1, b'3']);
17829        let mut wire = orphan_frame(1, 0x4, 1, head.len() as u32, &head);
17830        wire.extend(orphan_frame(0, 0, 1, 5, b"hello"));
17831        peer.write_all(&wire).expect("loopback write must complete");
17832        let reset = drive_and_read_backend(connection, peer, context, router);
17833        let reset = peer_frames(&reset).expect("whole frames");
17834        assert!(
17835            reset
17836                .iter()
17837                .any(|(kind, _, id, payload)| *kind == 3 && *id == 1 && payload == &[0, 0, 0, 1]),
17838            "premise: stream 1 is reset with PROTOCOL_ERROR, got {reset:?}"
17839        );
17840        linked
17841    }
17842
17843    /// The trailer block adding `x-dyn: one` to the HPACK dynamic table
17844    /// (literal with incremental indexing) on stream 1, in one HEADERS frame
17845    /// or, with `split`, a HEADERS frame without END_HEADERS and a
17846    /// CONTINUATION frame (RFC 9113 §6.10).
17847    fn dynamic_trailer_block(split: bool) -> Vec<u8> {
17848        let mut trailer = vec![0x40, 5];
17849        trailer.extend_from_slice(b"x-dyn");
17850        trailer.extend_from_slice(&[3]);
17851        trailer.extend_from_slice(b"one");
17852        if split {
17853            let (first, rest) = trailer.split_at(4);
17854            let mut wire = orphan_frame(1, 0x1, 1, first.len() as u32, first);
17855            wire.extend(orphan_frame(9, 0x4, 1, rest.len() as u32, rest));
17856            wire
17857        } else {
17858            orphan_frame(1, 0x4 | 0x1, 1, trailer.len() as u32, &trailer)
17859        }
17860    }
17861
17862    /// RFC 9113 §5.1: after sozu resets a backend stream, the backend may
17863    /// still send frames it queued before reading the RST_STREAM, and sozu
17864    /// MUST ignore them. Here the backend sends a trailer block, in one
17865    /// HEADERS frame or split over a CONTINUATION frame, and another DATA
17866    /// frame on the reset stream. The trailer block adds `x-dyn: one` to the
17867    /// HPACK dynamic table, and the response on stream 3 refers to that
17868    /// entry, so it parses only if the ignored block was decoded whole
17869    /// (§4.3). What the backend had in flight counts no glitch.
17870    ///
17871    /// TO SEE THIS RED: in `ConnectionH2::handle_header_state`, drop the
17872    /// `was_reset_locally` branch: the trailer HEADERS is answered
17873    /// GOAWAY(STREAM_CLOSED); or, in the `H2State::Discard` arm,
17874    /// drop the `pending_discarded_block` store: the CONTINUATION is
17875    /// answered GOAWAY(PROTOCOL_ERROR) as standalone.
17876    #[test]
17877    fn frames_on_a_backend_stream_sozu_reset_are_ignored() {
17878        use std::io::Write;
17879
17880        for split in [true, false] {
17881            let LinkedBackend {
17882                _pool,
17883                mut connection,
17884                mut peer,
17885                mut context,
17886                mut router,
17887                ..
17888            } = backend_stream_reset_for_its_content_length();
17889            let glitches = connection.core.flood_detector.glitch_count();
17890
17891            let mut wire = dynamic_trailer_block(split);
17892            wire.extend(orphan_frame(0, 0x1, 1, 2, b"zz"));
17893            peer.write_all(&wire).expect("loopback write must complete");
17894            let after =
17895                drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
17896            let after = peer_frames(&after).expect("whole frames");
17897            assert!(
17898                !after.iter().any(|(kind, _, _, _)| *kind == 7 || *kind == 3),
17899                "split={split}: frames on the reset stream are ignored: no GOAWAY, no \
17900                 RST_STREAM, got {after:?}"
17901            );
17902            assert!(
17903                matches!(connection.core.state, H2State::Header),
17904                "split={split}: the shared connection stays up, got {:?}",
17905                connection.core.state
17906            );
17907            assert_eq!(
17908                connection.core.flood_detector.glitch_count(),
17909                glitches,
17910                "split={split}: what the backend had in flight counts no glitch"
17911            );
17912
17913            // A second stream, answered with `:status 200` and the dynamic
17914            // entry the ignored trailer block added (index 62).
17915            let second = context
17916                .create_stream(Ulid::generate(), 1 << 16)
17917                .expect("test context must create a stream");
17918            assert!(
17919                connection.start_stream(second, &mut context),
17920                "the backend connection accepts a second stream"
17921            );
17922            queue_request(&mut context, second);
17923            context.streams[second].state = StreamState::Link;
17924            let request =
17925                drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
17926            assert!(
17927                peer_frames(&request)
17928                    .expect("whole frames")
17929                    .iter()
17930                    .any(|(kind, _, id, _)| *kind == 1 && *id == 3),
17931                "premise: the second request went out on stream 3, got {request:?}"
17932            );
17933            peer.write_all(&orphan_frame(1, 0x4 | 0x1, 3, 2, &[0x88, 0xbe]))
17934                .expect("loopback write must complete");
17935            let last =
17936                drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
17937            let last = peer_frames(&last).expect("whole frames");
17938            assert!(
17939                !last.iter().any(|(kind, _, _, _)| *kind == 7),
17940                "split={split}: the second response decodes against the same dynamic table, \
17941                 got {last:?}"
17942            );
17943            let buffer = context.streams[second].back.storage.buffer();
17944            let has_dynamic_field = context.streams[second].back.blocks.iter().any(|block| {
17945                matches!(block, kawa::Block::Header(kawa::Pair { key, val })
17946                    if key.data(buffer) == b"x-dyn" && val.data(buffer) == b"one")
17947            });
17948            assert!(
17949                has_dynamic_field,
17950                "split={split}: the second response carries the field of the dynamic entry"
17951            );
17952        }
17953    }
17954
17955    /// DATA a backend sends on a stream Sōzu reset counts no glitch while it
17956    /// fits the stream's receive window, the 65 535 bytes Sōzu advertised
17957    /// and keeps crediting back: the backend may have had that much in
17958    /// flight when it read the reset (RFC 9113 §6.9). DATA beyond it counts
17959    /// one glitch per frame.
17960    ///
17961    /// TO SEE THIS RED: in `ConnectionH2::handle_header_state`, count a glitch for
17962    /// every frame ignored on a reset stream: `in-flight DATA counts no
17963    /// glitch`.
17964    #[test]
17965    fn data_on_a_reset_backend_stream_counts_a_glitch_beyond_its_window_only() {
17966        use std::io::Write;
17967
17968        let LinkedBackend {
17969            _pool,
17970            mut connection,
17971            mut peer,
17972            mut context,
17973            mut router,
17974            ..
17975        } = backend_stream_reset_for_its_content_length();
17976        let glitches = connection.core.flood_detector.glitch_count();
17977        // 5 bytes already arrived before the reset; four 16 000-byte frames
17978        // still fit in the 65 535-byte window.
17979        let chunk = vec![b'd'; 16_000];
17980        let mut wire = Vec::new();
17981        for _ in 0..4 {
17982            wire.extend(orphan_frame(0, 0, 1, chunk.len() as u32, &chunk));
17983        }
17984        peer.write_all(&wire).expect("loopback write must complete");
17985        drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
17986        assert_eq!(
17987            connection.core.flood_detector.glitch_count(),
17988            glitches,
17989            "in-flight DATA counts no glitch"
17990        );
17991
17992        peer.write_all(&orphan_frame(0, 0, 1, chunk.len() as u32, &chunk))
17993            .expect("loopback write must complete");
17994        let after = drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
17995        assert_eq!(
17996            connection.core.flood_detector.glitch_count(),
17997            glitches + 1,
17998            "DATA beyond the stream's window counts a glitch"
17999        );
18000        assert!(
18001            !peer_frames(&after)
18002                .expect("whole frames")
18003                .iter()
18004                .any(|(kind, _, _, _)| *kind == 7 || *kind == 3),
18005            "it is still ignored, not answered"
18006        );
18007    }
18008
18009    /// Header blocks a backend sends on a stream Sōzu reset count no glitch
18010    /// for the first two, a trailer section or an interim and a final
18011    /// response it may have had in flight (RFC 9113 §8.1), and one glitch
18012    /// each beyond: a peer repeating blocks on a reset stream trips the
18013    /// glitch limit and the connection ends with GOAWAY(ENHANCE_YOUR_CALM),
18014    /// instead of having every block HPACK-decoded for free.
18015    ///
18016    /// TO SEE THIS RED: in `ConnectionH2::handle_header_state`, count every header
18017    /// block on a reset stream as in flight: no GOAWAY ever comes.
18018    #[test]
18019    fn header_blocks_on_a_reset_backend_stream_beyond_two_count_glitches() {
18020        use std::io::Write;
18021
18022        let LinkedBackend {
18023            _pool,
18024            mut connection,
18025            mut peer,
18026            mut context,
18027            mut router,
18028            ..
18029        } = backend_stream_reset_for_its_content_length();
18030        // Each small write must reach the connection before the next drive,
18031        // not wait behind Nagle's algorithm for the previous one's ACK.
18032        peer.set_nodelay(true).expect("TCP_NODELAY must apply");
18033        // An explicit glitch limit, so the flood below trips it whatever the
18034        // default (`DEFAULT_MAX_GLITCH_COUNT`) is.
18035        connection.core.flood_detector = h2_flood_detector::H2FloodDetector::new(
18036            H2FloodConfig::from_optional(
18037                None,
18038                None,
18039                None,
18040                None,
18041                None,
18042                None,
18043                Some(100),
18044                None,
18045                None,
18046                None,
18047                None,
18048                None,
18049                None,
18050                None,
18051            ),
18052            connection.core.now,
18053        );
18054        let glitches = connection.core.flood_detector.glitch_count();
18055        // `:status 200`, alone, in one HEADERS frame ending the block.
18056        let block = || orphan_frame(1, 0x4, 1, 1, &[0x88]);
18057
18058        let mut wire = block();
18059        wire.extend(block());
18060        peer.write_all(&wire).expect("loopback write must complete");
18061        drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
18062        assert_eq!(
18063            connection.core.flood_detector.glitch_count(),
18064            glitches,
18065            "two header blocks may be in flight"
18066        );
18067
18068        peer.write_all(&block())
18069            .expect("loopback write must complete");
18070        drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
18071        assert_eq!(
18072            connection.core.flood_detector.glitch_count(),
18073            glitches + 1,
18074            "a third header block counts a glitch"
18075        );
18076
18077        let mut received = Vec::new();
18078        for _ in 0..20 {
18079            let mut wire = Vec::new();
18080            for _ in 0..100 {
18081                wire.extend(block());
18082            }
18083            if peer.write_all(&wire).is_err() {
18084                break;
18085            }
18086            received.extend(drive_and_read_backend(
18087                &mut connection,
18088                &mut peer,
18089                &mut context,
18090                &mut router,
18091            ));
18092            if peer_frames(&received)
18093                .map(|frames| frames.iter().any(|(kind, _, _, _)| *kind == 7))
18094                .unwrap_or(false)
18095            {
18096                break;
18097            }
18098        }
18099        let frames = peer_frames(&received).expect("whole frames");
18100        assert!(
18101            frames.iter().any(|(kind, _, _, payload)| *kind == 7
18102                && payload.get(4..8) == Some(&[0, 0, 0, 0xb][..])),
18103            "repeated header blocks end the connection with GOAWAY(ENHANCE_YOUR_CALM), got {frames:?}"
18104        );
18105    }
18106
18107    /// Empty DATA frames without END_STREAM on a stream Sōzu reset count
18108    /// toward the empty-DATA flood limit as on a live stream
18109    /// (CVE-2019-9518): they carry no payload to charge against the stream's
18110    /// window, so without it a peer could send them for free. A flood of
18111    /// them ends the connection with GOAWAY(ENHANCE_YOUR_CALM).
18112    ///
18113    /// TO SEE THIS RED: in `ConnectionH2::handle_header_state`, drop the
18114    /// `record_empty_data_frame` call of the `was_reset_locally` branch: no
18115    /// GOAWAY ever comes.
18116    #[test]
18117    fn empty_data_frames_on_a_reset_backend_stream_count_toward_the_flood_limit() {
18118        use std::io::Write;
18119
18120        let LinkedBackend {
18121            _pool,
18122            mut connection,
18123            mut peer,
18124            mut context,
18125            mut router,
18126            ..
18127        } = backend_stream_reset_for_its_content_length();
18128        peer.set_nodelay(true).expect("TCP_NODELAY must apply");
18129        // An explicit empty-DATA limit, so the flood below trips it whatever the
18130        // default (`DEFAULT_MAX_EMPTY_DATA_PER_WINDOW`) is. The glitch limit
18131        // stays the default: the GOAWAY must come from the empty-DATA count.
18132        connection.core.flood_detector = h2_flood_detector::H2FloodDetector::new(
18133            H2FloodConfig::from_optional(
18134                None,
18135                None,
18136                None,
18137                Some(100),
18138                None,
18139                None,
18140                None,
18141                None,
18142                None,
18143                None,
18144                None,
18145                None,
18146                None,
18147                None,
18148            ),
18149            connection.core.now,
18150        );
18151        let mut received = Vec::new();
18152        for _ in 0..50 {
18153            let mut wire = Vec::new();
18154            for _ in 0..100 {
18155                wire.extend(orphan_frame(0, 0, 1, 0, b""));
18156            }
18157            if peer.write_all(&wire).is_err() {
18158                break;
18159            }
18160            received.extend(drive_and_read_backend(
18161                &mut connection,
18162                &mut peer,
18163                &mut context,
18164                &mut router,
18165            ));
18166            if peer_frames(&received)
18167                .map(|frames| frames.iter().any(|(kind, _, _, _)| *kind == 7))
18168                .unwrap_or(false)
18169            {
18170                break;
18171            }
18172        }
18173        let frames = peer_frames(&received).expect("whole frames");
18174        assert!(
18175            frames.iter().any(|(kind, _, _, payload)| *kind == 7
18176                && payload.get(4..8) == Some(&[0, 0, 0, 0xb][..])),
18177            "a flood of empty DATA frames ends the connection with \
18178             GOAWAY(ENHANCE_YOUR_CALM), got {frames:?}"
18179        );
18180    }
18181
18182    /// A backend stream Sōzu reset from its write pass, because its request
18183    /// failed after its head left, stays tracked until its end: DATA the
18184    /// backend then sends on it is ignored (RFC 9113 §5.1), never queued for
18185    /// the client nor counted against the response.
18186    ///
18187    /// TO SEE THIS RED: in `ConnectionH2::handle_header_state`, drop the
18188    /// `was_reset_locally` branch: the DATA reaches the stream and is counted
18189    /// in `back_data_received`.
18190    #[test]
18191    fn data_on_a_tracked_backend_stream_sozu_reset_is_ignored() {
18192        use std::io::Write;
18193
18194        let LinkedBackend {
18195            _pool,
18196            mut connection,
18197            mut peer,
18198            mut context,
18199            mut router,
18200            gid,
18201        } = backend_with_a_linked_stream(
18202            H2State::Header,
18203            BackendStatus::Connected,
18204            Ready::READABLE | Ready::HUP | Ready::ERROR,
18205        );
18206        // A request whose body is still to come: its head leaves, the
18207        // stream stays open on the request side.
18208        queue_request(&mut context, gid);
18209        let front = &mut context.streams[gid].front;
18210        if let Some(kawa::Block::Flags(flags)) = front.blocks.back_mut() {
18211            flags.end_stream = false;
18212        }
18213        front.body_size = kawa::BodySize::Chunked;
18214        front.parsing_phase = kawa::ParsingPhase::Chunks { first: true };
18215        context.streams[gid].state = StreamState::Link;
18216        drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
18217        // The request side then fails: the write pass resets the stream and
18218        // keeps it tracked.
18219        context.streams[gid]
18220            .front
18221            .parsing_phase
18222            .error("request failed".into());
18223        connection.core.readiness.arm_writable();
18224        let reset = drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
18225        assert!(
18226            peer_frames(&reset)
18227                .expect("whole frames")
18228                .iter()
18229                .any(|(kind, _, id, _)| *kind == 3 && *id == 1),
18230            "premise: the write pass resets stream 1, got {reset:?}"
18231        );
18232        assert!(
18233            connection.core.stream_table.get(1).is_some()
18234                && connection.core.stream_table.rst_sent_contains(1),
18235            "premise: the reset stream is still tracked"
18236        );
18237
18238        peer.write_all(&orphan_frame(0, 0, 1, 5, b"hello"))
18239            .expect("loopback write must complete");
18240        let after = drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
18241        assert_eq!(
18242            context.streams[gid].back_data_received, 0,
18243            "the DATA is not counted against the response"
18244        );
18245        assert!(
18246            !context.streams[gid]
18247                .back
18248                .blocks
18249                .iter()
18250                .any(|block| matches!(block, kawa::Block::Chunk(_))),
18251            "the DATA is not queued for the client"
18252        );
18253        assert!(
18254            !peer_frames(&after)
18255                .expect("whole frames")
18256                .iter()
18257                .any(|(kind, _, _, _)| *kind == 7),
18258            "the connection stays up"
18259        );
18260    }
18261
18262    /// The client-facing side of RFC 9113 §5.1: a request framed by
18263    /// `content-length: 3` whose DATA carries 5 bytes is reset by Sōzu
18264    /// (§8.1.1); the DATA and the trailer block the client had already sent
18265    /// on that stream are ignored, and the connection stays up.
18266    ///
18267    /// TO SEE THIS RED: in `ConnectionH2::handle_header_state`, drop the
18268    /// `was_reset_locally` branch: the trailer HEADERS is answered
18269    /// GOAWAY(STREAM_CLOSED).
18270    #[test]
18271    fn frames_on_a_client_stream_sozu_reset_are_ignored() {
18272        use std::io::Write;
18273
18274        let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 20, 16_384)));
18275        let (mut connection, mut peer) = test_h2_connection(&pool, None);
18276        let mut context = test_context(&pool);
18277        let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
18278        connection.core.state = H2State::Header;
18279        connection
18280            .core
18281            .stream_table
18282            .set_expect_read(Some((H2StreamId::Zero, 9)));
18283        let block = crate::protocol::mux::hpack::Encoder::new().encode([
18284            (&b":method"[..], &b"POST"[..]),
18285            (&b":scheme"[..], &b"https"[..]),
18286            (&b":authority"[..], &b"example.com"[..]),
18287            (&b":path"[..], &b"/upload"[..]),
18288            (&b"content-length"[..], &b"3"[..]),
18289        ]);
18290        let mut wire = orphan_frame(1, parser::FLAG_END_HEADERS, 1, block.len() as u32, &block);
18291        wire.extend(orphan_frame(0, 0, 1, 5, b"hello"));
18292        peer.write_all(&wire).expect("loopback write must complete");
18293        let reset = drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
18294        let reset = peer_frames(&reset).expect("whole frames");
18295        assert!(
18296            reset
18297                .iter()
18298                .any(|(kind, _, id, payload)| *kind == 3 && *id == 1 && payload == &[0, 0, 0, 1]),
18299            "premise: stream 1 is reset with PROTOCOL_ERROR, got {reset:?}"
18300        );
18301
18302        let mut wire = orphan_frame(0, 0, 1, 2, b"zz");
18303        wire.extend(dynamic_trailer_block(false));
18304        peer.write_all(&wire).expect("loopback write must complete");
18305        let after = drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
18306        let after = peer_frames(&after).expect("whole frames");
18307        assert!(
18308            !after.iter().any(|(kind, _, _, _)| *kind == 7 || *kind == 3),
18309            "frames on the reset stream are ignored: no GOAWAY, no RST_STREAM, got {after:?}"
18310        );
18311        assert!(
18312            matches!(connection.core.state, H2State::Header),
18313            "the client connection stays up, got {:?}",
18314            connection.core.state
18315        );
18316    }
18317
18318    /// A stream Sōzu refuses at its HEADERS frame (here because
18319    /// SETTINGS_MAX_CONCURRENT_STREAMS is reached) is answered
18320    /// RST_STREAM(REFUSED_STREAM) and its field block discarded, and the
18321    /// CONTINUATION frame completing that block (RFC 9113 §6.10) is
18322    /// discarded with it, not taken for a standalone CONTINUATION.
18323    ///
18324    /// TO SEE THIS RED: in the `H2State::Discard` arm of
18325    /// [`ConnectionH2::handle_read`], drop the `pending_discarded_block`
18326    /// store: the CONTINUATION is answered GOAWAY(PROTOCOL_ERROR).
18327    #[test]
18328    fn a_continuation_of_a_refused_header_block_is_discarded_with_it() {
18329        use std::io::Write;
18330
18331        let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 20, 16_384)));
18332        let (mut connection, mut peer) = test_h2_connection(&pool, None);
18333        let mut context = test_context(&pool);
18334        let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
18335        connection.core.state = H2State::Header;
18336        connection
18337            .core
18338            .stream_table
18339            .set_expect_read(Some((H2StreamId::Zero, 9)));
18340        connection
18341            .core
18342            .local_settings
18343            .settings_max_concurrent_streams = 0;
18344        let block = crate::protocol::mux::hpack::Encoder::new().encode([
18345            (&b":method"[..], &b"GET"[..]),
18346            (&b":scheme"[..], &b"https"[..]),
18347            (&b":authority"[..], &b"example.com"[..]),
18348            (&b":path"[..], &b"/"[..]),
18349        ]);
18350        let (first, rest) = block.split_at(block.len() / 2);
18351        let mut wire = orphan_frame(1, parser::FLAG_END_STREAM, 1, first.len() as u32, first);
18352        wire.extend(orphan_frame(
18353            9,
18354            parser::FLAG_END_HEADERS,
18355            1,
18356            rest.len() as u32,
18357            rest,
18358        ));
18359        peer.write_all(&wire).expect("loopback write must complete");
18360        let answer = drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
18361        let answer = peer_frames(&answer).expect("whole frames");
18362        assert!(
18363            answer
18364                .iter()
18365                .any(|(kind, _, id, payload)| *kind == 3 && *id == 1 && payload == &[0, 0, 0, 7]),
18366            "the stream is refused, got {answer:?}"
18367        );
18368        assert!(
18369            !answer.iter().any(|(kind, _, _, _)| *kind == 7),
18370            "the CONTINUATION is discarded with its block, got {answer:?}"
18371        );
18372        assert!(
18373            matches!(connection.core.state, H2State::Header),
18374            "the client connection stays up, got {:?}",
18375            connection.core.state
18376        );
18377    }
18378
18379    /// A refused header block is held to `max_header_list_size` like an
18380    /// accepted one, its first fragment included: a 16 000-byte HEADERS frame
18381    /// and four 12 500-byte CONTINUATION frames exceed the 65 536-byte limit
18382    /// only with that first fragment, and end the connection with
18383    /// GOAWAY(ENHANCE_YOUR_CALM) (CVE-2024-27316).
18384    ///
18385    /// TO SEE THIS RED: in [`ConnectionH2::discard_field_block`], drop the
18386    /// `begin_header_block_if_new` call: the block is read to its end, and
18387    /// the test fails once its 30-second deadline passes without a GOAWAY.
18388    #[test]
18389    fn a_refused_header_block_counts_its_first_fragment_toward_its_size() {
18390        use std::io::Write;
18391
18392        let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 20, 16_384)));
18393        let (mut connection, mut peer) = test_h2_connection(&pool, None);
18394        let mut context = test_context(&pool);
18395        let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
18396        connection.core.state = H2State::Header;
18397        connection
18398            .core
18399            .stream_table
18400            .set_expect_read(Some((H2StreamId::Zero, 9)));
18401        connection
18402            .core
18403            .local_settings
18404            .settings_max_concurrent_streams = 0;
18405        let first = vec![0x88; 16_000];
18406        let mut wire = orphan_frame(1, 0, 1, first.len() as u32, &first);
18407        let rest = vec![0x88; 12_500];
18408        for _ in 0..4 {
18409            wire.extend(orphan_frame(9, 0, 1, rest.len() as u32, &rest));
18410        }
18411        // The peer cannot write all of it before the connection reads, so
18412        // write it from a thread while the connection is driven.
18413        let mut writer = peer.try_clone().expect("the peer socket must clone");
18414        let sender = std::thread::spawn(move || writer.write_all(&wire));
18415        let enhance_your_calm = |frames: &[PeerFrame]| {
18416            frames.iter().any(|(kind, _, _, payload)| {
18417                *kind == 7 && payload.get(4..8) == Some(&[0, 0, 0, 0xb][..])
18418            })
18419        };
18420        // Drive until the GOAWAY reaches the peer, not for a fixed number of
18421        // passes: on a loaded host the writer thread can be descheduled for
18422        // all of them, and the GOAWAY the read of the last fragment queues is
18423        // only flushed by the next pass's `writable()`.
18424        let deadline = Instant::now() + Duration::from_secs(30);
18425        let mut received = Vec::new();
18426        while Instant::now() < deadline {
18427            received.extend(drive_and_read_backend(
18428                &mut connection,
18429                &mut peer,
18430                &mut context,
18431                &mut router,
18432            ));
18433            if peer_frames(&received).is_ok_and(|frames| enhance_your_calm(&frames)) {
18434                break;
18435            }
18436        }
18437        sender
18438            .join()
18439            .expect("the writer thread must not panic")
18440            .expect("the peer must write the whole block");
18441        let frames = peer_frames(&received).expect("whole frames");
18442        assert!(
18443            enhance_your_calm(&frames),
18444            "the oversized refused block ends the connection with GOAWAY(ENHANCE_YOUR_CALM), \
18445             got {frames:?}"
18446        );
18447    }
18448
18449    /// sozu-proxy/sozu#1631, reaper form: `cancel_timed_out_streams` has the
18450    /// same shape as `end_stream` for a backend stream reaped before its
18451    /// HEADERS left, and must forget it the same way.
18452    ///
18453    /// TO SEE THIS RED: in [`ConnectionH2::cancel_timed_out_streams`], make
18454    /// `never_opened` `false`. The stream is then reset: `a stream reaped
18455    /// before its HEADERS left must not be reset`. Red on `d5161919`,
18456    /// verified 2026-09-28.
18457    #[test]
18458    fn a_backend_stream_reaped_before_its_headers_left_is_not_reset() {
18459        let LinkedBackend {
18460            _pool,
18461            mut connection,
18462            mut peer,
18463            mut context,
18464            mut router,
18465            gid: _,
18466        } = backend_with_a_linked_stream(
18467            H2State::Header,
18468            BackendStatus::Connected,
18469            Ready::READABLE | Ready::HUP | Ready::ERROR,
18470        );
18471        connection.core.readiness.interest.remove(Ready::WRITABLE);
18472
18473        context.now += Duration::from_secs(31);
18474        connection.cancel_timed_out_streams(&mut context, &mut EndpointClient(&mut router));
18475        assert!(
18476            connection.core.stream_table.is_empty(),
18477            "premise: the idle guard reaped stream 1"
18478        );
18479        assert!(
18480            connection.core.control_tx.pending().is_empty()
18481                && !connection.core.readiness.interest.is_writable()
18482                && connection.core.output.is_empty(),
18483            "a stream reaped before its HEADERS left must not be reset"
18484        );
18485        let received =
18486            drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
18487        assert_eq!(
18488            received,
18489            Vec::<u8>::new(),
18490            "and nothing reaches the backend"
18491        );
18492    }
18493
18494    /// sozu-proxy/sozu#1631, frontend-error form (found by the adversarial
18495    /// review of the first fix): the H1 frontend rejects a malformed request
18496    /// body and answers 400 itself, which leaves the request kawa in `Error`
18497    /// while its backend stream is still registered and its HEADERS never
18498    /// left. The next write pass used to hand it to the converter, whose
18499    /// `initialize` queues a RST_STREAM for that idle stream.
18500    ///
18501    /// TO SEE THIS RED: in [`ConnectionH2::poll_write_target`]'s
18502    /// `H2WritePhase::Prepare` arm, make `never_opened_error` `false`. The
18503    /// backend then reads `RST_STREAM(1)`: `no frame may name stream 1`.
18504    /// Red on `6eef0ba1`, verified 2026-09-28.
18505    #[test]
18506    fn a_backend_stream_whose_request_failed_before_its_headers_left_is_not_reset() {
18507        let LinkedBackend {
18508            _pool,
18509            mut connection,
18510            mut peer,
18511            mut context,
18512            mut router,
18513            gid,
18514        } = backend_with_a_linked_stream(
18515            H2State::Header,
18516            BackendStatus::Connected,
18517            Ready::READABLE | Ready::HUP | Ready::ERROR,
18518        );
18519        queue_request(&mut context, gid);
18520        // What `kawa::h1::parse` leaves on a malformed chunked body.
18521        context.streams[gid]
18522            .front
18523            .parsing_phase
18524            .error(kawa::ParsingErrorKind::Processing {
18525                message: "malformed chunk",
18526            });
18527        assert!(
18528            context.streams[gid].front.is_error() && !context.streams[gid].front.consumed,
18529            "premise: the request failed before any byte left"
18530        );
18531
18532        let received =
18533            drive_and_read_backend(&mut connection, &mut peer, &mut context, &mut router);
18534
18535        let frames = peer_frames(&received).expect("whole frames");
18536        assert!(
18537            frames.iter().all(|(_, _, id, _)| *id != 1),
18538            "no frame may name stream 1, which the backend never saw: {frames:?}"
18539        );
18540        assert!(
18541            matches!(connection.core.state, H2State::Header),
18542            "the shared connection stays up, got {:?}",
18543            connection.core.state
18544        );
18545    }
18546
18547    /// sozu-proxy/sozu#1631, parked form: the stream's HEADERS were encoded
18548    /// by a write pass, but the socket took none of it, so the block is
18549    /// parked in `front.out` (the #1627 situation on `Position::Client`).
18550    /// The backend has still not seen the stream: no RST for it, and since
18551    /// #1630 the dropped block resets the encoder's table, so the next
18552    /// stream's block still decodes on the backend.
18553    ///
18554    /// TO SEE THIS RED: same edit as
18555    /// `a_backend_stream_ended_before_its_headers_left_is_not_reset`. The
18556    /// backend then reads a RST_STREAM for stream 1, which it never opened:
18557    /// `no frame may name stream 1`. Red on `d5161919`, verified 2026-09-28.
18558    #[test]
18559    fn a_backend_stream_ended_with_its_headers_parked_is_not_reset() {
18560        let pool = make_pool_for_invariant_16();
18561        let (socket, _peer) = connected_socket();
18562        let backend = Rc::new(RefCell::new(crate::backends::Backend::new(
18563            "parked-backend",
18564            "127.0.0.1:2".parse().expect("backend address must parse"),
18565            None,
18566            None,
18567            None,
18568        )));
18569        let mut registry = crate::protocol::mux::BackendRegistry::default();
18570        let mut connection = H2Shell::new(
18571            Ulid::generate(),
18572            PacedSocket {
18573                stream: socket,
18574                budget: 0,
18575                wire: Vec::new(),
18576                inbound: std::collections::VecDeque::new(),
18577            },
18578            Position::Client(
18579                "parked-cluster".into(),
18580                registry.id_for(&backend),
18581                BackendStatus::Connected,
18582            ),
18583            &mut PoolBufferSource::new(Rc::downgrade(&pool)),
18584            H2FloodConfig::default(),
18585            H2ConnectionConfig::default(),
18586            Duration::from_secs(30),
18587            None,
18588            Duration::from_secs(30),
18589            Some((H2StreamId::Zero, 9)),
18590            Ready::READABLE | Ready::WRITABLE | Ready::HUP | Ready::ERROR,
18591        )
18592        .expect("a pool with free buffers must yield an H2 connection");
18593        connection.core.state = H2State::Header;
18594        let mut context = test_context(&pool);
18595        let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
18596        let first = context
18597            .create_stream(Ulid::generate(), 1 << 16)
18598            .expect("test context must create a stream");
18599        queue_request(&mut context, first);
18600        assert!(
18601            connection.start_stream(first, &mut context),
18602            "premise: stream 1"
18603        );
18604        connection.core.readiness.event.insert(Ready::WRITABLE);
18605        connection.writable(&mut context, EndpointClient(&mut router));
18606        assert!(
18607            connection.socket.wire.is_empty()
18608                && !context.streams[first].front.out.is_empty()
18609                && !context.streams[first].front.consumed,
18610            "premise: stream 1's HEADERS were encoded and parked unsent"
18611        );
18612
18613        connection.end_stream(first, &mut context);
18614        let third = context
18615            .create_stream(Ulid::generate(), 1 << 16)
18616            .expect("test context must create a stream");
18617        queue_request(&mut context, third);
18618        assert!(
18619            connection.start_stream(third, &mut context),
18620            "premise: stream 3"
18621        );
18622        connection.socket.budget = usize::MAX;
18623        drive_both_ways(&mut connection, &mut context, &mut router);
18624
18625        let frames = peer_frames(&connection.socket.wire).expect("whole frames");
18626        assert!(
18627            frames.iter().all(|(_, _, id, _)| *id != 1),
18628            "no frame may name stream 1, which the backend never saw: {frames:?}"
18629        );
18630        let mut decoder = crate::protocol::mux::hpack::Decoder::new();
18631        let decoded: Vec<(u32, bool)> = frames
18632            .iter()
18633            .filter(|(kind, ..)| *kind == 1)
18634            .map(|(_, _, id, payload)| (*id, decoder.decode_with_cb(payload, |_, _| {}).is_ok()))
18635            .collect();
18636        assert_eq!(
18637            decoded,
18638            vec![(3, true)],
18639            "stream 3's block must decode with the backend's own table"
18640        );
18641    }
18642
18643    /// #1597, with #1604's split: skipping an orphaned remainder turns
18644    /// READABLE back on even while output is queued, and leaves that output
18645    /// alone. The skip reads into `zero`, which is input only, so there is
18646    /// nothing to wait for; the queued frame still leaves in order.
18647    ///
18648    /// Replaces the pre-#1604 test that pinned the opposite (READABLE kept
18649    /// off while zero held output), for the pre-image, where `zero` also held
18650    /// our output and a read into it would have parsed that output.
18651    ///
18652    /// TO SEE THIS RED: in [`ConnectionH2::skip_orphaned_data_payload`], only
18653    /// insert READABLE when `self.output.is_empty()`. The test fails with
18654    /// `the skip must read on while output is queued`.
18655    #[test]
18656    fn skipping_an_orphaned_payload_reads_on_while_output_is_queued() {
18657        let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 20, 16_384)));
18658        let (mut connection, _peer) = test_h2_connection(&pool, None);
18659        let window_update = orphan_frame(8, 0, 1, 4, &16_384u32.to_be_bytes());
18660        connection.core.output.push(&window_update);
18661        connection.core.readiness.interest.remove(Ready::READABLE);
18662
18663        connection.core.skip_orphaned_data_payload(6, 10);
18664
18665        assert!(
18666            connection.core.readiness.interest.is_readable(),
18667            "the skip must read on while output is queued"
18668        );
18669        assert_eq!(
18670            connection.core.stream_table.expect_read(),
18671            Some((H2StreamId::Zero, 6)),
18672            "the remainder must still be scheduled for skipping"
18673        );
18674        assert_eq!(
18675            connection.core.output_pending(),
18676            &window_update[..],
18677            "the skip must leave the queued output untouched"
18678        );
18679    }
18680
18681    /// A backend (client-side) H2 connection starts write-only, sends its
18682    /// preface and SETTINGS, and must then READ, for the server's SETTINGS.
18683    /// The pre-image turned READABLE on as a side effect of flushing `zero`;
18684    /// with the output queue separate from the input, the
18685    /// `(ClientSettings, Client)` arm turns it on itself.
18686    ///
18687    /// TO SEE THIS RED: in [`ConnectionH2::dispatch_writable_state`]'s
18688    /// `(H2State::ClientSettings, Position::Client(..))` arm, delete
18689    /// `self.readiness.interest.insert(Ready::READABLE);`. The test fails
18690    /// with `a backend connection must read once its preface is out`.
18691    #[test]
18692    fn a_backend_connection_reads_once_its_preface_is_out() {
18693        use std::io::Read;
18694
18695        let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 20, 16_384)));
18696        let (socket, mut peer) = connected_socket();
18697        let backend = Rc::new(RefCell::new(crate::backends::Backend::new(
18698            "preface-backend",
18699            "127.0.0.1:2".parse().expect("backend address must parse"),
18700            None,
18701            None,
18702            None,
18703        )));
18704        let mut registry = crate::protocol::mux::BackendRegistry::default();
18705        let mut connection = H2Shell::new(
18706            Ulid::generate(),
18707            socket,
18708            Position::Client(
18709                "preface-cluster".into(),
18710                registry.id_for(&backend),
18711                BackendStatus::Connected,
18712            ),
18713            &mut PoolBufferSource::new(Rc::downgrade(&pool)),
18714            H2FloodConfig::default(),
18715            H2ConnectionConfig::default(),
18716            Duration::from_secs(30),
18717            None,
18718            Duration::from_secs(30),
18719            None,
18720            Ready::WRITABLE | Ready::HUP | Ready::ERROR,
18721        )
18722        .expect("a pool with free buffers must yield an H2 connection");
18723        let mut context = test_context(&pool);
18724        let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
18725
18726        for _ in 0..2 {
18727            connection.core.readiness.event.insert(Ready::WRITABLE);
18728            connection.writable(&mut context, EndpointClient(&mut router));
18729        }
18730
18731        let mut preface = [0u8; 24];
18732        peer.read_exact(&mut preface)
18733            .expect("the backend must receive the client preface");
18734        assert_eq!(&preface[..], serializer::H2_PRI.as_bytes());
18735        assert!(
18736            matches!(connection.core.state, H2State::ServerSettings),
18737            "premise: the preface went out, got {:?}",
18738            connection.core.state
18739        );
18740        assert!(
18741            connection.core.readiness.interest.is_readable(),
18742            "a backend connection must read once its preface is out"
18743        );
18744    }
18745
18746    /// A peer that sends while never reading cannot grow the output queue
18747    /// without bound: once one buffer's worth is queued, reading stops, as
18748    /// HAProxy's demux stops on a full `mbuf` (`H2_CF_DEM_MROOM`), and it
18749    /// resumes once the queue drains below that.
18750    ///
18751    /// TO SEE THIS RED: in [`ConnectionH2::poll_read_target`], delete the
18752    /// `self.output.len() >= self.output_read_cap()` early return. The test
18753    /// fails with `a full output queue must stop reading`.
18754    #[test]
18755    fn a_full_output_queue_stops_reading_until_it_drains() {
18756        let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 20, 16_384)));
18757        let (mut connection, _peer) = test_h2_connection(&pool, None);
18758        let mut context = test_context(&pool);
18759        let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
18760        connection.core.state = H2State::Header;
18761        connection
18762            .core
18763            .stream_table
18764            .set_expect_read(Some((H2StreamId::Zero, 9)));
18765        let cap = connection.core.output_read_cap();
18766        connection.core.output.push(&vec![0; cap]);
18767        connection.core.readiness.interest.insert(Ready::READABLE);
18768        connection.core.readiness.event.insert(Ready::READABLE);
18769
18770        connection.readable(&mut context, EndpointClient(&mut router));
18771
18772        assert!(
18773            !connection.core.readiness.interest.is_readable(),
18774            "a full output queue must stop reading"
18775        );
18776        assert!(
18777            connection.core.readiness.event.is_readable(),
18778            "the READABLE event must be kept for the read that follows the drain"
18779        );
18780        connection.core.consume_output(1);
18781        assert!(
18782            connection.core.readiness.interest.is_readable(),
18783            "draining below the cap must restore reading"
18784        );
18785    }
18786
18787    /// Clearing the output queue, as a peer that hung up while the
18788    /// connection drains does, also ends the read cap's hold on READABLE:
18789    /// otherwise `reads_wait_for_output` would outlive the output it waited on
18790    /// and the shutdown drive's reads would stay withdrawn.
18791    ///
18792    /// TO SEE THIS RED: in `ConnectionH2::clear_output`, drop everything but
18793    /// `self.output.clear();`. The test then fails with `a cleared queue must
18794    /// release the read cap`. Verified 2026-09-27.
18795    #[test]
18796    fn clearing_the_output_releases_the_read_cap() {
18797        let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 20, 16_384)));
18798        let (mut connection, _peer) = test_h2_connection(&pool, None);
18799        let mut context = test_context(&pool);
18800        let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
18801        connection.core.state = H2State::Header;
18802        connection
18803            .core
18804            .stream_table
18805            .set_expect_read(Some((H2StreamId::Zero, 9)));
18806        let cap = connection.core.output_read_cap();
18807        connection.core.output.push(&vec![0; cap]);
18808        connection.core.readiness.interest.insert(Ready::READABLE);
18809        connection.core.readiness.event.insert(Ready::READABLE);
18810        connection.readable(&mut context, EndpointClient(&mut router));
18811        assert!(
18812            connection.core.reads_wait_for_output
18813                && !connection.core.readiness.interest.is_readable(),
18814            "premise: the read cap withdrew READABLE"
18815        );
18816
18817        connection.core.clear_output();
18818
18819        assert!(
18820            !connection.core.reads_wait_for_output
18821                && connection.core.readiness.interest.is_readable(),
18822            "a cleared queue must release the read cap"
18823        );
18824    }
18825
18826    /// #1597, with #1604's split: a frame header read while output is still
18827    /// queued lands in `zero` and leaves that output intact, and the answer
18828    /// to the frame read queues BEHIND it. A stalled WINDOW_UPDATE flush
18829    /// leaves exactly that state; the CI e2e suite once caught the
18830    /// pre-image, where both shared `zero`, through `handle_header_state`'s
18831    /// `debug_assert!`, with `zero` holding a stream-1 WINDOW_UPDATE followed
18832    /// by the peer's next DATA header.
18833    ///
18834    /// Replaces the pre-#1604 test that pinned the pre-image's cure (no frame
18835    /// header read at all while zero held unflushed output until the flush):
18836    /// the hazard it guarded no longer exists, and what the peer must receive
18837    /// is unchanged and still asserted below.
18838    ///
18839    /// TO SEE THIS RED: in [`ConnectionH2::handle_ping_frame`], queue the ACK
18840    /// in front of the output instead of behind it (`self.output.clear()`
18841    /// before `push_frames`). The test fails with `the PING ACK must queue
18842    /// behind the WINDOW_UPDATE`.
18843    #[test]
18844    fn a_frame_header_read_leaves_queued_output_intact() {
18845        use std::io::{Read, Write};
18846
18847        let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 20, 16_384)));
18848        let (mut connection, mut peer) = test_h2_connection(&pool, None);
18849        peer.set_nonblocking(true)
18850            .expect("the loopback peer must switch to non-blocking");
18851        let mut context = test_context(&pool);
18852        let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
18853        connection.core.state = H2State::Header;
18854        connection
18855            .core
18856            .stream_table
18857            .set_expect_read(Some((H2StreamId::Zero, 9)));
18858
18859        // What a stalled WINDOW_UPDATE flush leaves behind.
18860        let window_update = orphan_frame(8, 0, 1, 4, &16_384u32.to_be_bytes());
18861        connection.core.output.push(&window_update);
18862        connection.core.readiness.interest.insert(Ready::READABLE);
18863
18864        peer.write_all(&orphan_ping())
18865            .expect("loopback write must complete");
18866        let mut expected = window_update.clone();
18867        expected.extend(orphan_frame(6, parser::FLAG_ACK, 0, 8, b"pingpong"));
18868        for _ in 0..64 {
18869            connection.core.readiness.event.insert(Ready::READABLE);
18870            connection.readable(&mut context, EndpointClient(&mut router));
18871            if connection.core.output.len() >= expected.len() {
18872                break;
18873            }
18874            std::thread::yield_now();
18875        }
18876        assert_eq!(
18877            connection.core.output_pending(),
18878            &expected[..],
18879            "the PING ACK must queue behind the WINDOW_UPDATE"
18880        );
18881
18882        // Driven the way `Mux` does, the queue leaves in order.
18883        let mut received = Vec::new();
18884        for _ in 0..256 {
18885            connection
18886                .core
18887                .readiness
18888                .event
18889                .insert(Ready::READABLE | Ready::WRITABLE);
18890            connection.core.readiness.interest.insert(Ready::WRITABLE);
18891            if connection.core.readiness.filter_interest().is_writable() {
18892                connection.writable(&mut context, EndpointClient(&mut router));
18893            }
18894            if connection.core.readiness.filter_interest().is_readable() {
18895                connection.readable(&mut context, EndpointClient(&mut router));
18896            }
18897            let mut buf = [0u8; 256];
18898            if let Ok(n) = peer.read(&mut buf) {
18899                received.extend_from_slice(&buf[..n]);
18900            }
18901            if received.len() >= expected.len() {
18902                break;
18903            }
18904            std::thread::yield_now();
18905        }
18906        assert_eq!(
18907            received, expected,
18908            "the peer must receive the WINDOW_UPDATE, then the PING ACK"
18909        );
18910    }
18911
18912    /// The RED-confirmed test above sends a HEADERS payload that is nothing
18913    /// but a field block: no `Pad Length`, no `Stream Dependency`/`Weight`.
18914    /// That shape cannot catch correction #1 — RFC 9113 §6.2 lays a HEADERS
18915    /// payload out as `[Pad Length?][Priority?][field block][padding?]`, and
18916    /// `refuse_stream_and_discard`'s new-stream callers pass the *whole*
18917    /// payload. A decode that forgets to strip the PADDED/PRIORITY prefix
18918    /// feeds the HPACK decoder six bytes of pad-length/dependency/weight
18919    /// garbage before the real field block starts.
18920    ///
18921    /// To SEE THIS RED: in [`decode_discarded_field_block`]'s
18922    /// `DiscardedFieldBlock::New` arm, replace the `let fragment = headers
18923    /// .header_block_fragment.data_opt(payload)...` lines with `let fragment
18924    /// = payload;` — i.e. hand the connection decoder the raw payload
18925    /// unstripped, the way a fix that only handled the un-padded case would.
18926    /// The peer's first block is still legitimate HPACK once you skip past
18927    /// the 6-byte prefix, but decoding from byte 0 instead makes the pad
18928    /// length byte and the first three dependency/weight octets look like
18929    /// HPACK opcodes. Verified 2026-09-21: the decode fails with
18930    /// `H2Error::CompressionError`, observed via the connection reaching
18931    /// `H2State::GoAway` (the panic is on the "connection usable" assertion,
18932    /// not the later decode assertion — the malformed prefix corrupts the
18933    /// connection before the probe even gets to send its second block).
18934    #[test]
18935    fn a_refused_padded_prioritized_stream_keeps_the_hpack_decoder_in_sync() {
18936        use std::io::Write;
18937
18938        let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 20, 16_384)));
18939        let (mut connection, mut peer) = test_h2_connection(&pool, None);
18940        let mut context = test_context(&pool);
18941        let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
18942
18943        connection.core.state = H2State::Header;
18944        connection
18945            .core
18946            .stream_table
18947            .set_expect_read(Some((H2StreamId::Zero, 9)));
18948        connection.core.drain.__test_set_draining();
18949
18950        // Same probe technique as the sibling test: this appends
18951        // `x-sozu-probe: alpha` to the peer's dynamic table at index 62.
18952        let mut peer_encoder = crate::protocol::mux::hpack::Encoder::new();
18953        let field_block = peer_encoder.encode([
18954            (&b":method"[..], &b"GET"[..]),
18955            (&b":scheme"[..], &b"https"[..]),
18956            (&b":authority"[..], &b"example.com"[..]),
18957            (&b":path"[..], &b"/refused"[..]),
18958            (&b"x-sozu-probe"[..], &b"alpha"[..]),
18959        ]);
18960
18961        // RFC 9113 §6.2 HEADERS payload: Pad Length (1) + Stream Dependency
18962        // (4, exclusive bit clear, depends on stream 0) + Weight (1) + the
18963        // field block + 3 zero padding bytes.
18964        const PAD_LEN: u8 = 3;
18965        let mut payload = Vec::with_capacity(1 + 5 + field_block.len() + PAD_LEN as usize);
18966        payload.push(PAD_LEN);
18967        payload.extend_from_slice(&0u32.to_be_bytes()); // Stream Dependency
18968        payload.push(15); // Weight
18969        payload.extend_from_slice(&field_block);
18970        payload.extend(std::iter::repeat_n(0u8, PAD_LEN as usize));
18971
18972        // HEADERS, END_STREAM | END_HEADERS | PADDED | PRIORITY, stream 1.
18973        let mut frame = Vec::with_capacity(9 + payload.len());
18974        frame.extend_from_slice(&(payload.len() as u32).to_be_bytes()[1..]);
18975        frame.push(1);
18976        frame.push(
18977            parser::FLAG_END_STREAM
18978                | parser::FLAG_END_HEADERS
18979                | parser::FLAG_PADDED
18980                | parser::FLAG_PRIORITY,
18981        );
18982        frame.extend_from_slice(&1u32.to_be_bytes());
18983        frame.extend_from_slice(&payload);
18984        peer.write_all(&frame)
18985            .expect("loopback write must complete");
18986        peer.flush().expect("loopback flush must complete");
18987
18988        for _ in 0..64 {
18989            connection.readable(&mut context, EndpointClient(&mut router));
18990            if matches!(connection.core.state, H2State::Header)
18991                && matches!(
18992                    connection.core.stream_table.expect_read(),
18993                    Some((H2StreamId::Zero, 9))
18994                )
18995                && !connection.core.control_tx.pending().is_empty()
18996            {
18997                break;
18998            }
18999            std::thread::yield_now();
19000        }
19001
19002        assert_eq!(
19003            connection.core.control_tx.pending(),
19004            vec![(1, H2Error::RefusedStream)],
19005            "the drain gate must refuse stream 1 with RST_STREAM(REFUSED_STREAM)"
19006        );
19007        assert!(
19008            connection.core.stream_table.is_empty(),
19009            "a refused stream must not be created"
19010        );
19011        assert!(
19012            !matches!(connection.core.state, H2State::Error | H2State::GoAway),
19013            "refusing a PADDED|PRIORITY HEADERS frame must leave the connection \
19014             usable: {:?}",
19015            connection.core.state
19016        );
19017
19018        let next_block = peer_encoder.encode([(&b"x-sozu-probe"[..], &b"alpha"[..])]);
19019        assert_eq!(
19020            next_block.len(),
19021            1,
19022            "the peer must now reference its dynamic entry, not re-send a literal"
19023        );
19024
19025        let mut decoded = Vec::new();
19026        let status = connection
19027            .core
19028            .hpack
19029            .decoder_mut()
19030            .decode_with_cb(&next_block, |k, v| {
19031                decoded.push((k.into_owned(), v.into_owned()));
19032            });
19033
19034        assert!(
19035            status.is_ok(),
19036            "the connection decoder must still resolve the peer's dynamic table \
19037             after a refused PADDED|PRIORITY stream, got {status:?}"
19038        );
19039        assert_eq!(
19040            decoded,
19041            vec![(b"x-sozu-probe".to_vec(), b"alpha".to_vec())],
19042            "the refused stream's field block — located past the Pad Length and \
19043             Priority prefix — must have been decoded into the connection's \
19044             HPACK context"
19045        );
19046    }
19047
19048    // ── `self.zero` is read-accumulation AND write scratch — that is the bug ──
19049    //
19050    // `ConnectionH2::zero` serves two roles: the read-side accumulation
19051    // buffer for a HEADERS/CONTINUATION field block in progress, and the
19052    // write scratch space `flush_pending_control_frames` reuses to
19053    // serialise ANY queued WINDOW_UPDATE or RST_STREAM — for any stream,
19054    // not just the one being reassembled. `mod.rs`'s inner event loop
19055    // dispatches frontend `readable()` then `writable()` in the same sweep,
19056    // so a legitimate, non-refused multi-frame header block can be
19057    // clobbered by completely unrelated connection-level flow-control
19058    // housekeeping before its CONTINUATION frame ever arrives — no
19059    // adversarial peer required.
19060    //
19061    // That was the pre-image of #1396/#1397/#1401 and, for the write
19062    // scratch, of #1604: the accumulator moved to `header_reassembly`, and
19063    // every control frame is now queued in the separate `output`. The tests
19064    // below keep pinning the outcome, a block that survives every
19065    // interleaved flush; their RED notes describe the tree they were written
19066    // against.
19067
19068    /// To SEE THIS RED: this is the pre-existing behaviour on `main`, no
19069    /// mutation needed. `flush_pending_control_frames`'s WINDOW_UPDATE-drain
19070    /// stage (`let kawa = &mut self.zero; kawa.storage.clear();`) runs
19071    /// unconditionally whenever a WINDOW_UPDATE is queued, with no check on
19072    /// `self.state`. Guarding that stage (and the RST_STREAM-drain stage
19073    /// beneath it) on `matches!(self.state, H2State::ContinuationHeader(_) |
19074    /// H2State::ContinuationFrame(_))` is exactly the fix this test proves.
19075    #[test]
19076    fn a_legitimate_continuation_survives_an_unrelated_window_update_flush() {
19077        use std::io::Write;
19078
19079        let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 20, 16_384)));
19080        let (mut connection, mut peer) = test_h2_connection(&pool, None);
19081        let mut context = test_context(&pool);
19082        let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
19083
19084        // Past the preface/SETTINGS handshake, waiting on a frame header.
19085        // `drain.draining` stays false: stream 1 must be genuinely accepted,
19086        // not refused — this is the ordinary, non-refusal path.
19087        connection.core.state = H2State::Header;
19088        connection
19089            .core
19090            .stream_table
19091            .set_expect_read(Some((H2StreamId::Zero, 9)));
19092
19093        // The peer's encoder. The marker header's name is in neither table,
19094        // so encoding it appends `x-sozu-marker: legitimate-value` to the
19095        // peer's dynamic table at a known index — the same technique
19096        // `a_refused_stream_keeps_the_hpack_decoder_in_sync` uses to prove
19097        // decoder sync, here applied to a stream that is never refused.
19098        let mut peer_encoder = crate::protocol::mux::hpack::Encoder::new();
19099        let field_block = peer_encoder.encode([
19100            (&b":method"[..], &b"GET"[..]),
19101            (&b":scheme"[..], &b"https"[..]),
19102            (&b":authority"[..], &b"example.com"[..]),
19103            (&b":path"[..], &b"/legit-multiframe"[..]),
19104            (&b"x-sozu-marker"[..], &b"legitimate-value"[..]),
19105        ]);
19106        // Split well past the 13 bytes a single WINDOW_UPDATE frame occupies,
19107        // so the clobber this test provokes lands inside real field-block
19108        // bytes rather than in a margin this test invented.
19109        assert!(
19110            field_block.len() > 26,
19111            "the probe block must be large enough to split meaningfully"
19112        );
19113        let split = field_block.len() / 2;
19114        let (first_half, second_half) = field_block.split_at(split);
19115
19116        // HEADERS, END_STREAM but NOT END_HEADERS, stream 1: a legitimate
19117        // multi-frame header block, exactly as a large request produces.
19118        let mut headers_frame = Vec::with_capacity(9 + first_half.len());
19119        headers_frame.extend_from_slice(&(first_half.len() as u32).to_be_bytes()[1..]);
19120        headers_frame.push(1); // HEADERS
19121        headers_frame.push(parser::FLAG_END_STREAM);
19122        headers_frame.extend_from_slice(&1u32.to_be_bytes());
19123        headers_frame.extend_from_slice(first_half);
19124        peer.write_all(&headers_frame)
19125            .expect("loopback write must complete");
19126        peer.flush().expect("loopback flush must complete");
19127
19128        // Drive reads until the connection is genuinely waiting on the
19129        // CONTINUATION frame for stream 1 — accepted, not refused.
19130        for _ in 0..64 {
19131            connection.readable(&mut context, EndpointClient(&mut router));
19132            if matches!(connection.core.state, H2State::ContinuationHeader(_)) {
19133                break;
19134            }
19135            std::thread::yield_now();
19136        }
19137        assert!(
19138            matches!(connection.core.state, H2State::ContinuationHeader(_)),
19139            "stream 1's HEADERS frame must be accepted and await CONTINUATION: {:?}",
19140            connection.core.state
19141        );
19142        assert!(
19143            !connection.core.stream_table.is_empty(),
19144            "a non-refused HEADERS frame must create stream 1"
19145        );
19146
19147        // Completely unrelated connection-level housekeeping: replenishing
19148        // the connection flow-control window after DATA consumed on some
19149        // other stream. Nothing about it targets stream 1 or its
19150        // in-progress header block — this is the ordinary path, not an
19151        // attack.
19152        connection.core.queue_window_update(0, 65_535);
19153        connection.writable(&mut context, EndpointClient(&mut router));
19154
19155        // Now the CONTINUATION frame completing the block arrives.
19156        let mut continuation_frame = Vec::with_capacity(9 + second_half.len());
19157        continuation_frame.extend_from_slice(&(second_half.len() as u32).to_be_bytes()[1..]);
19158        continuation_frame.push(9); // CONTINUATION
19159        continuation_frame.push(parser::FLAG_END_HEADERS);
19160        continuation_frame.extend_from_slice(&1u32.to_be_bytes());
19161        continuation_frame.extend_from_slice(second_half);
19162        peer.write_all(&continuation_frame)
19163            .expect("loopback write must complete");
19164        peer.flush().expect("loopback flush must complete");
19165
19166        for _ in 0..64 {
19167            connection.readable(&mut context, EndpointClient(&mut router));
19168            if !matches!(
19169                connection.core.state,
19170                H2State::ContinuationHeader(_) | H2State::ContinuationFrame(_)
19171            ) {
19172                break;
19173            }
19174            std::thread::yield_now();
19175        }
19176
19177        assert!(
19178            !matches!(connection.core.state, H2State::Error | H2State::GoAway),
19179            "an unrelated WINDOW_UPDATE flush must not corrupt a legitimate \
19180             in-progress header block into a connection error: {:?}",
19181            connection.core.state
19182        );
19183
19184        // The peer's next block references the dynamic entry its encoder
19185        // added while encoding stream 1's request.
19186        let next_block = peer_encoder.encode([(&b"x-sozu-marker"[..], &b"legitimate-value"[..])]);
19187        assert_eq!(
19188            next_block.len(),
19189            1,
19190            "the peer must now reference its dynamic entry, not re-send a literal"
19191        );
19192
19193        let mut decoded = Vec::new();
19194        let status = connection
19195            .core
19196            .hpack
19197            .decoder_mut()
19198            .decode_with_cb(&next_block, |k, v| {
19199                decoded.push((k.into_owned(), v.into_owned()));
19200            });
19201
19202        assert!(
19203            status.is_ok(),
19204            "the connection decoder must still resolve the peer's dynamic table \
19205             after an unrelated WINDOW_UPDATE flush during header-block \
19206             reassembly, got {status:?}"
19207        );
19208        assert_eq!(
19209            decoded,
19210            vec![(b"x-sozu-marker".to_vec(), b"legitimate-value".to_vec())],
19211            "stream 1's field block must have been decoded byte-for-byte, not \
19212             clobbered by the unrelated WINDOW_UPDATE flush"
19213        );
19214    }
19215
19216    /// Same shape as
19217    /// `a_legitimate_continuation_survives_an_unrelated_window_update_flush`,
19218    /// but the clobbering trigger is `graceful_goaway` itself instead of an
19219    /// unrelated WINDOW_UPDATE flush — the third instance of the same bug
19220    /// class (issue #1398).
19221    ///
19222    /// Since #1637 the advisory GOAWAY is no longer deferred while the block
19223    /// reassembles: it is queued in `self.output` and flushed in the same
19224    /// pass, and this test checks both halves on the wire — the peer reads
19225    /// the 17-byte `GOAWAY(NO_ERROR, STREAM_ID_MAX)` BEFORE it sends the
19226    /// CONTINUATION, and the block that CONTINUATION completes still decodes
19227    /// byte-for-byte afterwards.
19228    ///
19229    /// To SEE THIS RED, either reintroduce the #1401 deferral (hold the
19230    /// GOAWAY back while `H2State::ContinuationHeader` holds) — the
19231    /// `read_exact` of the advisory GOAWAY before the CONTINUATION finds nothing
19232    /// — or serialize the GOAWAY into `self.zero.storage` as the pre-#1604
19233    /// code did, which corrupts the block and fails the decode assertions.
19234    #[test]
19235    fn a_legitimate_continuation_survives_a_graceful_goaway() {
19236        use std::io::{Read, Write};
19237
19238        let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 20, 16_384)));
19239        let (mut connection, mut peer) = test_h2_connection(&pool, None);
19240        let mut context = test_context(&pool);
19241        let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
19242
19243        // Past the preface/SETTINGS handshake, waiting on a frame header.
19244        // `drain.draining` stays false until `graceful_goaway` is called
19245        // below: stream 1 must be genuinely accepted, not refused — this is
19246        // the ordinary, non-refusal path.
19247        connection.core.state = H2State::Header;
19248        connection
19249            .core
19250            .stream_table
19251            .set_expect_read(Some((H2StreamId::Zero, 9)));
19252
19253        // The peer's encoder. The marker header's name is in neither table,
19254        // so encoding it appends `x-sozu-marker: legitimate-value` to the
19255        // peer's dynamic table at a known index — the same technique
19256        // `a_legitimate_continuation_survives_an_unrelated_window_update_flush`
19257        // uses to prove decoder sync.
19258        let mut peer_encoder = crate::protocol::mux::hpack::Encoder::new();
19259        let field_block = peer_encoder.encode([
19260            (&b":method"[..], &b"GET"[..]),
19261            (&b":scheme"[..], &b"https"[..]),
19262            (&b":authority"[..], &b"example.com"[..]),
19263            (&b":path"[..], &b"/legit-multiframe"[..]),
19264            (&b"x-sozu-marker"[..], &b"legitimate-value"[..]),
19265        ]);
19266        // Same header set and split proportions as
19267        // `a_legitimate_continuation_survives_an_unrelated_window_update_flush`:
19268        // `header_block_fragment`'s window is a `(start, len)` pair recorded
19269        // once, independent of `self.zero.storage`'s own bookkeeping, so
19270        // *any* clear-then-refill of that buffer — 13 bytes of WINDOW_UPDATE
19271        // or 17 bytes of GOAWAY, it makes no difference — leaves the window
19272        // pointing at the same wrong offset once `end`/`head` reset to 0 and
19273        // the CONTINUATION's own bytes land there instead. Matching the split
19274        // reproduces the same decodable-but-wrong HPACK byte sequence rather
19275        // than a split that happens to land on an invalid opcode.
19276        assert!(
19277            field_block.len() > 34,
19278            "the probe block must be large enough to split meaningfully"
19279        );
19280        let split = field_block.len() / 2;
19281        let (first_half, second_half) = field_block.split_at(split);
19282
19283        // HEADERS, END_STREAM but NOT END_HEADERS, stream 1: a legitimate
19284        // multi-frame header block, exactly as a large request produces.
19285        let mut headers_frame = Vec::with_capacity(9 + first_half.len());
19286        headers_frame.extend_from_slice(&(first_half.len() as u32).to_be_bytes()[1..]);
19287        headers_frame.push(1); // HEADERS
19288        headers_frame.push(parser::FLAG_END_STREAM);
19289        headers_frame.extend_from_slice(&1u32.to_be_bytes());
19290        headers_frame.extend_from_slice(first_half);
19291        peer.write_all(&headers_frame)
19292            .expect("loopback write must complete");
19293        peer.flush().expect("loopback flush must complete");
19294
19295        // Drive reads until the connection is genuinely waiting on the
19296        // CONTINUATION frame for stream 1 — accepted, not refused.
19297        for _ in 0..64 {
19298            connection.readable(&mut context, EndpointClient(&mut router));
19299            if matches!(connection.core.state, H2State::ContinuationHeader(_)) {
19300                break;
19301            }
19302            std::thread::yield_now();
19303        }
19304        assert!(
19305            matches!(connection.core.state, H2State::ContinuationHeader(_)),
19306            "stream 1's HEADERS frame must be accepted and await CONTINUATION: {:?}",
19307            connection.core.state
19308        );
19309        assert!(
19310            !connection.core.stream_table.is_empty(),
19311            "a non-refused HEADERS frame must create stream 1"
19312        );
19313
19314        // The proxy decides to drain — a graceful shutdown or hot reload
19315        // landing mid-reassembly, exactly as issue #1398 describes. Nothing
19316        // about it targets stream 1 or its in-progress header block.
19317        let drain_at = connection.core.now;
19318        let result = connection.graceful_goaway(drain_at);
19319        assert!(
19320            matches!(result, MuxResult::Continue),
19321            "graceful_goaway must not tear the connection down mid-reassembly: {result:?}"
19322        );
19323
19324        // The forced-close budget must arm from the moment the proxy decided
19325        // to drain — `Mux::shutting_down` samples exactly this field against
19326        // `graceful_shutdown_deadline`.
19327        assert!(
19328            connection.core.drain.draining(),
19329            "graceful_goaway must mark the connection as draining immediately"
19330        );
19331        assert_eq!(
19332            connection.core.drain.__test_started_at(),
19333            Some(drain_at),
19334            "the forced-close budget must arm from `graceful_goaway`'s `now` parameter"
19335        );
19336        assert!(
19337            matches!(connection.core.state, H2State::ContinuationHeader(_)),
19338            "queueing the advisory GOAWAY must leave the reassembly state alone: {:?}",
19339            connection.core.state
19340        );
19341
19342        // Mirror `Mux::shutting_down_inner` exactly: it calls
19343        // `flush_output_buffer()` immediately after `graceful_goaway()`
19344        // returns `Continue`, because edge-triggered epoll won't deliver a
19345        // fresh WRITABLE event for an already-writable socket. The advisory
19346        // GOAWAY leaves in this flush, while the block is still incomplete.
19347        connection.flush_output_buffer();
19348        assert!(
19349            connection.core.output.is_empty(),
19350            "the advisory GOAWAY must have been fully flushed to the socket"
19351        );
19352
19353        let mut expected = [0u8; 17];
19354        let (_, expected_size) =
19355            serializer::gen_goaway(&mut expected, STREAM_ID_MAX, H2Error::NoError)
19356                .expect("serializing the expected GOAWAY must succeed");
19357        assert_eq!(expected_size, 17);
19358
19359        let mut received = [0u8; 17];
19360        peer.set_read_timeout(Some(Duration::from_secs(5)))
19361            .expect("setting a read timeout must succeed");
19362        peer.read_exact(&mut received)
19363            .expect("the peer must receive the advisory GOAWAY before it completes the block");
19364        assert_eq!(
19365            received, expected,
19366            "the advisory GOAWAY must reach the peer byte-for-byte while its \
19367             header block is still incomplete"
19368        );
19369
19370        // Now the CONTINUATION frame completing the block arrives.
19371        let mut continuation_frame = Vec::with_capacity(9 + second_half.len());
19372        continuation_frame.extend_from_slice(&(second_half.len() as u32).to_be_bytes()[1..]);
19373        continuation_frame.push(9); // CONTINUATION
19374        continuation_frame.push(parser::FLAG_END_HEADERS);
19375        continuation_frame.extend_from_slice(&1u32.to_be_bytes());
19376        continuation_frame.extend_from_slice(second_half);
19377        peer.write_all(&continuation_frame)
19378            .expect("loopback write must complete");
19379        peer.flush().expect("loopback flush must complete");
19380
19381        for _ in 0..64 {
19382            connection.readable(&mut context, EndpointClient(&mut router));
19383            if !matches!(
19384                connection.core.state,
19385                H2State::ContinuationHeader(_) | H2State::ContinuationFrame(_)
19386            ) {
19387                break;
19388            }
19389            std::thread::yield_now();
19390        }
19391
19392        assert!(
19393            !matches!(connection.core.state, H2State::Error | H2State::GoAway),
19394            "a graceful_goaway landing mid-reassembly must not corrupt a \
19395             legitimate in-progress header block into a connection error: {:?}",
19396            connection.core.state
19397        );
19398
19399        // The peer's next block references the dynamic entry its encoder
19400        // added while encoding stream 1's request.
19401        let next_block = peer_encoder.encode([(&b"x-sozu-marker"[..], &b"legitimate-value"[..])]);
19402        assert_eq!(
19403            next_block.len(),
19404            1,
19405            "the peer must now reference its dynamic entry, not re-send a literal"
19406        );
19407
19408        let mut decoded = Vec::new();
19409        let status = connection
19410            .core
19411            .hpack
19412            .decoder_mut()
19413            .decode_with_cb(&next_block, |k, v| {
19414                decoded.push((k.into_owned(), v.into_owned()));
19415            });
19416
19417        assert!(
19418            status.is_ok(),
19419            "the connection decoder must still resolve the peer's dynamic table \
19420             after a graceful_goaway during header-block reassembly, got {status:?}"
19421        );
19422        assert_eq!(
19423            decoded,
19424            vec![(b"x-sozu-marker".to_vec(), b"legitimate-value".to_vec())],
19425            "stream 1's field block must have been decoded byte-for-byte, not \
19426             clobbered by graceful_goaway"
19427        );
19428
19429        // Nothing else was owed to the peer: the drain is not a second
19430        // GOAWAY, and the completed block leaves nothing queued.
19431        assert!(
19432            connection.core.output.is_empty(),
19433            "completing the block must not queue a second advisory GOAWAY"
19434        );
19435    }
19436
19437    /// Regression test for #1423: `flush_pending_control_frames`'s
19438    /// `frontend_hung_up_while_draining()` stage cleared `zero.storage`
19439    /// unconditionally, with no `header_block_reassembly_in_progress()`
19440    /// guard — unlike its three sibling stages, which #1397 and #1401
19441    /// already gated. LIFECYCLE.md invariant 24 tracked this as a "known
19442    /// live gap, reported and NOT fixed" until this changeset.
19443    ///
19444    /// Same shape as `a_legitimate_continuation_survives_a_graceful_goaway`,
19445    /// but the clobbering trigger is a HUP/ERROR readiness event landing
19446    /// while draining — exactly `frontend_hung_up_while_draining()`'s own
19447    /// condition — instead of `graceful_goaway` itself. This test drives
19448    /// the gap BETWEEN two frames (a HUP arriving while
19449    /// `expect_read = Some((Zero, 9))`, waiting for the next CONTINUATION's
19450    /// header, with nothing of this block's history left in `zero.storage`
19451    /// to lose). Moving the accumulator out of `zero.storage` into
19452    /// `self.header_reassembly` (`h2_header_reassembly.rs`) makes THIS
19453    /// scenario safe regardless of whether this stage is guarded: there is
19454    /// no accumulated history left in `zero.storage` for it to clobber.
19455    ///
19456    /// That is NOT the whole of #1423, though — see
19457    /// `a_continuation_frame_split_by_tcp_segmentation_survives_a_hup_while_draining`
19458    /// below for the narrower "HUP lands mid-frame, not between frames"
19459    /// case this test does not cover, which review of this changeset's
19460    /// first version (`e1c3c2fb`) proved the accumulator move alone does
19461    /// NOT make safe, and which is why `frontend_hung_up_while_draining()`'s
19462    /// stage carries the same `header_block_reassembly_in_progress()` guard
19463    /// as its three siblings after all.
19464    ///
19465    /// To SEE THIS RED on this step's base commit (`f76c8eb3`, before the
19466    /// accumulator moved out of `zero.storage`): run this test unmodified
19467    /// against that commit. `flush_pending_control_frames`'s first stage
19468    /// clears `zero.storage` — which, on that commit, still holds stream 1's
19469    /// entire accumulated first-half fragment — before the CONTINUATION
19470    /// frame completing the block arrives; the CONTINUATION's own bytes
19471    /// then land at the wrong (reset-to-zero) offset, and the connection's
19472    /// HPACK decoder desyncs from the peer's encoder. The failure is
19473    /// silent corruption, not a crash (HPACK carries no self-check): the
19474    /// `decode_with_cb` assertion below fails with a decode error, or
19475    /// succeeds but yields something other than
19476    /// `[(b"x-sozu-marker", b"legitimate-value")]`.
19477    #[test]
19478    fn a_legitimate_continuation_survives_a_hup_while_draining() {
19479        use std::io::Write;
19480
19481        let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 20, 16_384)));
19482        let (mut connection, mut peer) = test_h2_connection(&pool, None);
19483        let mut context = test_context(&pool);
19484        let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
19485
19486        // Past the preface/SETTINGS handshake, waiting on a frame header.
19487        // `drain.draining` stays false until manually forced below: stream 1
19488        // must be genuinely accepted, not refused — this is the ordinary,
19489        // non-refusal path.
19490        connection.core.state = H2State::Header;
19491        connection
19492            .core
19493            .stream_table
19494            .set_expect_read(Some((H2StreamId::Zero, 9)));
19495
19496        // The peer's encoder. The marker header's name is in neither table,
19497        // so encoding it appends `x-sozu-marker: legitimate-value` to the
19498        // peer's dynamic table at a known index — the same technique the
19499        // sibling WINDOW_UPDATE/graceful_goaway tests use to prove decoder
19500        // sync.
19501        let mut peer_encoder = crate::protocol::mux::hpack::Encoder::new();
19502        let field_block = peer_encoder.encode([
19503            (&b":method"[..], &b"GET"[..]),
19504            (&b":scheme"[..], &b"https"[..]),
19505            (&b":authority"[..], &b"example.com"[..]),
19506            (&b":path"[..], &b"/legit-multiframe"[..]),
19507            (&b"x-sozu-marker"[..], &b"legitimate-value"[..]),
19508        ]);
19509        assert!(
19510            field_block.len() > 34,
19511            "the probe block must be large enough to split meaningfully"
19512        );
19513        let split = field_block.len() / 2;
19514        let (first_half, second_half) = field_block.split_at(split);
19515
19516        // HEADERS, END_STREAM but NOT END_HEADERS, stream 1: a legitimate
19517        // multi-frame header block, exactly as a large request produces.
19518        let mut headers_frame = Vec::with_capacity(9 + first_half.len());
19519        headers_frame.extend_from_slice(&(first_half.len() as u32).to_be_bytes()[1..]);
19520        headers_frame.push(1); // HEADERS
19521        headers_frame.push(parser::FLAG_END_STREAM);
19522        headers_frame.extend_from_slice(&1u32.to_be_bytes());
19523        headers_frame.extend_from_slice(first_half);
19524        peer.write_all(&headers_frame)
19525            .expect("loopback write must complete");
19526        peer.flush().expect("loopback flush must complete");
19527
19528        // Drive reads until the connection is genuinely waiting on the
19529        // CONTINUATION frame for stream 1 — accepted, not refused.
19530        for _ in 0..64 {
19531            connection.readable(&mut context, EndpointClient(&mut router));
19532            if matches!(connection.core.state, H2State::ContinuationHeader(_)) {
19533                break;
19534            }
19535            std::thread::yield_now();
19536        }
19537        assert!(
19538            matches!(connection.core.state, H2State::ContinuationHeader(_)),
19539            "stream 1's HEADERS frame must be accepted and await CONTINUATION: {:?}",
19540            connection.core.state
19541        );
19542        assert!(
19543            !connection.core.stream_table.is_empty(),
19544            "a non-refused HEADERS frame must create stream 1"
19545        );
19546
19547        // Exactly #1423's trigger: the connection is draining AND the
19548        // frontend readiness reports HUP, landing squarely mid-reassembly.
19549        // A peer can legitimately produce this by sending TCP FIN together
19550        // with (or shortly after) the last CONTINUATION frame it manages to
19551        // get out, observed by edge-triggered epoll as one combined event.
19552        connection.core.drain.__test_set_draining();
19553        connection.core.readiness.event.insert(Ready::HUP);
19554
19555        // `flush_pending_control_frames`'s first, HUP-gated stage fires
19556        // here.
19557        connection.writable(&mut context, EndpointClient(&mut router));
19558
19559        // Real epoll delivers HUP once; un-signal it so the reads below
19560        // reflect the peer's normal traffic rather than a second synthetic
19561        // HUP this test injected by hand.
19562        connection.core.readiness.event.remove(Ready::HUP);
19563
19564        // Now the CONTINUATION frame completing the block arrives — still
19565        // fully deliverable, exactly as it would be if the peer's FIN and
19566        // its last frame arrived close together but the block itself was
19567        // sent in full.
19568        let mut continuation_frame = Vec::with_capacity(9 + second_half.len());
19569        continuation_frame.extend_from_slice(&(second_half.len() as u32).to_be_bytes()[1..]);
19570        continuation_frame.push(9); // CONTINUATION
19571        continuation_frame.push(parser::FLAG_END_HEADERS);
19572        continuation_frame.extend_from_slice(&1u32.to_be_bytes());
19573        continuation_frame.extend_from_slice(second_half);
19574        peer.write_all(&continuation_frame)
19575            .expect("loopback write must complete");
19576        peer.flush().expect("loopback flush must complete");
19577
19578        for _ in 0..64 {
19579            connection.readable(&mut context, EndpointClient(&mut router));
19580            if !matches!(
19581                connection.core.state,
19582                H2State::ContinuationHeader(_) | H2State::ContinuationFrame(_)
19583            ) {
19584                break;
19585            }
19586            std::thread::yield_now();
19587        }
19588
19589        assert!(
19590            !matches!(connection.core.state, H2State::Error | H2State::GoAway),
19591            "a HUP-while-draining event landing mid-reassembly must not corrupt \
19592             a legitimate in-progress header block into a connection error: {:?}",
19593            connection.core.state
19594        );
19595
19596        // The peer's next block references the dynamic entry its encoder
19597        // added while encoding stream 1's request.
19598        let next_block = peer_encoder.encode([(&b"x-sozu-marker"[..], &b"legitimate-value"[..])]);
19599        assert_eq!(
19600            next_block.len(),
19601            1,
19602            "the peer must now reference its dynamic entry, not re-send a literal"
19603        );
19604
19605        let mut decoded = Vec::new();
19606        let status = connection
19607            .core
19608            .hpack
19609            .decoder_mut()
19610            .decode_with_cb(&next_block, |k, v| {
19611                decoded.push((k.into_owned(), v.into_owned()));
19612            });
19613
19614        assert!(
19615            status.is_ok(),
19616            "the connection decoder must still resolve the peer's dynamic table \
19617             after a HUP-while-draining event during header-block reassembly, \
19618             got {status:?}"
19619        );
19620        assert_eq!(
19621            decoded,
19622            vec![(b"x-sozu-marker".to_vec(), b"legitimate-value".to_vec())],
19623            "stream 1's field block must have been decoded byte-for-byte, not \
19624             clobbered by the HUP-while-draining stage of \
19625             flush_pending_control_frames"
19626        );
19627    }
19628
19629    /// Regression test for B1 (review of `e1c3c2fb`): `handle_headers_frame`'s
19630    /// RFC 7540 §5.3.1 self-dependency early return — `reset_stream` +
19631    /// `remove_dead_stream` when a stream's own PRIORITY depends on itself —
19632    /// used to skip retiring `self.header_reassembly` before returning. When
19633    /// the aborted stream's block had gone through CONTINUATION reassembly
19634    /// (`header_reassembly.is_in_progress() == true` at abort time), the
19635    /// flag leaked into the very NEXT HEADERS frame processed on this
19636    /// connection: that frame's own "read from `zero.storage`" fast path
19637    /// was silently skipped in favour of the aborted stream's stale,
19638    /// already-discarded bytes — corrupting an entirely unrelated stream's
19639    /// decoded request. This reproduces in BOTH debug and release builds:
19640    /// nothing on this path was ever a `debug_assert!`.
19641    ///
19642    /// To SEE THIS RED on `e1c3c2fb` (before this fix): stream 1's block
19643    /// completes reassembly, its RFC 7540 PRIORITY self-dependency
19644    /// (`stream_dependency.stream_id == 1`) aborts it via `reset_stream`
19645    /// without retiring the accumulator, and
19646    /// `header_reassembly.is_in_progress()` stays `true`. Stream 3's own,
19647    /// unrelated, single-frame request is then decoded from stream 1's
19648    /// stale fragment instead of its own bytes: the `:path` pseudo-header
19649    /// this test asserts on is corrupted rather than
19650    /// `"/stream-3-legitimate"`.
19651    #[test]
19652    fn a_priority_self_dependency_reset_does_not_leak_the_reassembly_accumulator() {
19653        use std::io::Write;
19654
19655        let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 20, 16_384)));
19656        let (mut connection, mut peer) = test_h2_connection(&pool, None);
19657        let mut context = test_context(&pool);
19658        let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
19659
19660        connection.core.state = H2State::Header;
19661        connection
19662            .core
19663            .stream_table
19664            .set_expect_read(Some((H2StreamId::Zero, 9)));
19665
19666        // Stream 1: a legitimate multi-frame header block — split across
19667        // HEADERS + CONTINUATION, exactly like the sibling reassembly tests
19668        // — but the FIRST frame also carries an RFC 7540 PRIORITY field
19669        // whose stream dependency is stream 1 itself (RFC 7540 §5.3.1: a
19670        // stream cannot depend on itself).
19671        let mut peer_encoder = crate::protocol::mux::hpack::Encoder::new();
19672        let field_block = peer_encoder.encode([
19673            (&b":method"[..], &b"GET"[..]),
19674            (&b":scheme"[..], &b"https"[..]),
19675            (&b":authority"[..], &b"example.com"[..]),
19676            (&b":path"[..], &b"/stream-1-aborted"[..]),
19677        ]);
19678        assert!(
19679            field_block.len() > 10,
19680            "the probe block must be large enough to split meaningfully"
19681        );
19682        let split = field_block.len() / 2;
19683        let (first_half, second_half) = field_block.split_at(split);
19684
19685        // HEADERS, PRIORITY (self-dependency), NOT END_HEADERS.
19686        let mut priority_prefix = Vec::with_capacity(5);
19687        priority_prefix.extend_from_slice(&1u32.to_be_bytes()); // stream_dependency = 1 (self)
19688        priority_prefix.push(15); // weight
19689        let mut headers_frame = Vec::with_capacity(9 + priority_prefix.len() + first_half.len());
19690        headers_frame.extend_from_slice(
19691            &((priority_prefix.len() + first_half.len()) as u32).to_be_bytes()[1..],
19692        );
19693        headers_frame.push(1); // HEADERS
19694        headers_frame.push(parser::FLAG_PRIORITY);
19695        headers_frame.extend_from_slice(&1u32.to_be_bytes());
19696        headers_frame.extend_from_slice(&priority_prefix);
19697        headers_frame.extend_from_slice(first_half);
19698        peer.write_all(&headers_frame)
19699            .expect("loopback write must complete");
19700        peer.flush().expect("loopback flush must complete");
19701
19702        for _ in 0..64 {
19703            connection.readable(&mut context, EndpointClient(&mut router));
19704            if matches!(connection.core.state, H2State::ContinuationHeader(_)) {
19705                break;
19706            }
19707            std::thread::yield_now();
19708        }
19709        assert!(
19710            matches!(connection.core.state, H2State::ContinuationHeader(_)),
19711            "stream 1's HEADERS frame must be accepted and await CONTINUATION: {:?}",
19712            connection.core.state
19713        );
19714
19715        // Complete the block — this re-enters `handle_headers_frame`, which
19716        // detects the self-dependency and aborts stream 1.
19717        let mut continuation_frame = Vec::with_capacity(9 + second_half.len());
19718        continuation_frame.extend_from_slice(&(second_half.len() as u32).to_be_bytes()[1..]);
19719        continuation_frame.push(9); // CONTINUATION
19720        continuation_frame.push(parser::FLAG_END_HEADERS);
19721        continuation_frame.extend_from_slice(&1u32.to_be_bytes());
19722        continuation_frame.extend_from_slice(second_half);
19723        peer.write_all(&continuation_frame)
19724            .expect("loopback write must complete");
19725        peer.flush().expect("loopback flush must complete");
19726
19727        for _ in 0..64 {
19728            connection.readable(&mut context, EndpointClient(&mut router));
19729            if connection.core.stream_table.get(1).is_none() {
19730                break;
19731            }
19732            std::thread::yield_now();
19733        }
19734
19735        // Premise: stream 1 really was torn down for self-dependency.
19736        assert!(
19737            connection.core.stream_table.get(1).is_none(),
19738            "the self-dependent stream must have been torn down"
19739        );
19740
19741        // The direct proof of the fix: the accumulator must be idle again
19742        // before any other stream's HEADERS frame is processed. `h2.rs`'s
19743        // own test module can reach the private field directly — no
19744        // test-only accessor needed.
19745        assert!(
19746            !connection.core.header_reassembly.is_in_progress(),
19747            "aborting a self-dependent stream mid-reassembly must retire the \
19748             accumulator, not leave it `is_in_progress() == true` for the \
19749             next HEADERS frame on this connection"
19750        );
19751
19752        // Stream 3: an entirely unrelated, single-frame, complete request.
19753        let stream3_block = peer_encoder.encode([
19754            (&b":method"[..], &b"GET"[..]),
19755            (&b":scheme"[..], &b"https"[..]),
19756            (&b":authority"[..], &b"example.com"[..]),
19757            (&b":path"[..], &b"/stream-3-legitimate"[..]),
19758        ]);
19759        let mut stream3_frame = Vec::with_capacity(9 + stream3_block.len());
19760        stream3_frame.extend_from_slice(&(stream3_block.len() as u32).to_be_bytes()[1..]);
19761        stream3_frame.push(1); // HEADERS
19762        stream3_frame.push(parser::FLAG_END_STREAM | parser::FLAG_END_HEADERS);
19763        stream3_frame.extend_from_slice(&3u32.to_be_bytes());
19764        stream3_frame.extend_from_slice(&stream3_block);
19765        peer.write_all(&stream3_frame)
19766            .expect("loopback write must complete");
19767        peer.flush().expect("loopback flush must complete");
19768
19769        // Wait until the connection is back at `Header`, waiting on the
19770        // NEXT frame's 9-byte header — not merely until the stream slot
19771        // exists, which `create_stream` populates as soon as the frame
19772        // HEADER is parsed, well before its payload is read and decoded.
19773        for _ in 0..64 {
19774            connection.readable(&mut context, EndpointClient(&mut router));
19775            if matches!(connection.core.state, H2State::Header)
19776                && matches!(
19777                    connection.core.stream_table.expect_read(),
19778                    Some((H2StreamId::Zero, 9))
19779                )
19780            {
19781                break;
19782            }
19783            std::thread::yield_now();
19784        }
19785
19786        let stream3_gid = connection
19787            .core
19788            .stream_table
19789            .get(3)
19790            .expect("stream 3's HEADERS frame must create a stream");
19791        assert_eq!(
19792            context.streams[stream3_gid].context.path.as_deref(),
19793            Some("/stream-3-legitimate"),
19794            "stream 3 must decode its OWN bytes, not stream 1's stale, \
19795             aborted reassembly fragment"
19796        );
19797    }
19798
19799    /// Regression test for B2 (review of `e1c3c2fb`): `flush_pending_control_frames`'s
19800    /// `frontend_hung_up_while_draining()` stage used to clear `zero.storage`
19801    /// unconditionally, on the premise that `Ready::HUP` means "no further
19802    /// bytes can ever arrive". That premise is false: `Ready::HUP` is
19803    /// `is_read_closed() || is_write_closed()` (`command/src/ready.rs`), and
19804    /// mio documents `is_read_closed()` as also true on a TCP half-close —
19805    /// a FIN with data the peer already sent still sitting, unread, in the
19806    /// kernel receive queue. A CONTINUATION frame's payload split across two
19807    /// TCP segments can have its FIRST segment already read into
19808    /// `zero.storage` (a genuine partial `socket_read()`, not stale bytes
19809    /// from a completed pass) when the readiness event batches HUP together
19810    /// with that first segment; `drive_frontend_shutdown_io` (`mod.rs`)
19811    /// force-calls `readable()` for H2 on every `shutting_down()` poll, so
19812    /// this is the ordinary soft-stop path, not a contrived corner case.
19813    ///
19814    /// To SEE THIS RED on `e1c3c2fb` (before this fix): the partial first
19815    /// segment is wiped by the unconditional clear; the second segment then
19816    /// lands at the wrong (reset-to-zero) offset once appended, and the
19817    /// connection's HPACK decoder desyncs from the peer's encoder — the
19818    /// same silent-wrong-values failure mode as #1397/#1401/#1423.
19819    #[test]
19820    fn a_continuation_frame_split_by_tcp_segmentation_survives_a_hup_while_draining() {
19821        use std::io::Write;
19822
19823        let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 20, 16_384)));
19824        let (mut connection, mut peer) = test_h2_connection(&pool, None);
19825        let mut context = test_context(&pool);
19826        let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
19827
19828        connection.core.state = H2State::Header;
19829        connection
19830            .core
19831            .stream_table
19832            .set_expect_read(Some((H2StreamId::Zero, 9)));
19833
19834        let mut peer_encoder = crate::protocol::mux::hpack::Encoder::new();
19835        let field_block = peer_encoder.encode([
19836            (&b":method"[..], &b"GET"[..]),
19837            (&b":scheme"[..], &b"https"[..]),
19838            (&b":authority"[..], &b"example.com"[..]),
19839            (&b":path"[..], &b"/tcp-segmented"[..]),
19840            (&b"x-sozu-marker"[..], &b"legitimate-value"[..]),
19841        ]);
19842        assert!(
19843            field_block.len() > 40,
19844            "the probe block must be large enough to split meaningfully"
19845        );
19846        let split = field_block.len() / 2;
19847        let (first_half, continuation_payload) = field_block.split_at(split);
19848        assert!(
19849            continuation_payload.len() > 10,
19850            "the CONTINUATION frame's own payload must be large enough to \
19851             split into two TCP segments"
19852        );
19853        let payload_split = continuation_payload.len() / 2;
19854        let (payload_segment_1, payload_segment_2) = continuation_payload.split_at(payload_split);
19855
19856        // HEADERS, END_STREAM but NOT END_HEADERS, stream 1.
19857        let mut headers_frame = Vec::with_capacity(9 + first_half.len());
19858        headers_frame.extend_from_slice(&(first_half.len() as u32).to_be_bytes()[1..]);
19859        headers_frame.push(1); // HEADERS
19860        headers_frame.push(parser::FLAG_END_STREAM);
19861        headers_frame.extend_from_slice(&1u32.to_be_bytes());
19862        headers_frame.extend_from_slice(first_half);
19863        peer.write_all(&headers_frame)
19864            .expect("loopback write must complete");
19865        peer.flush().expect("loopback flush must complete");
19866
19867        for _ in 0..64 {
19868            connection.readable(&mut context, EndpointClient(&mut router));
19869            if matches!(connection.core.state, H2State::ContinuationHeader(_)) {
19870                break;
19871            }
19872            std::thread::yield_now();
19873        }
19874        assert!(
19875            matches!(connection.core.state, H2State::ContinuationHeader(_)),
19876            "stream 1's HEADERS frame must be accepted and await CONTINUATION: {:?}",
19877            connection.core.state
19878        );
19879
19880        // The CONTINUATION frame header, announcing the FULL payload
19881        // length, followed by only its FIRST TCP segment.
19882        let mut continuation_header_and_segment_1 = Vec::with_capacity(9 + payload_segment_1.len());
19883        continuation_header_and_segment_1
19884            .extend_from_slice(&(continuation_payload.len() as u32).to_be_bytes()[1..]);
19885        continuation_header_and_segment_1.push(9); // CONTINUATION
19886        continuation_header_and_segment_1.push(parser::FLAG_END_HEADERS);
19887        continuation_header_and_segment_1.extend_from_slice(&1u32.to_be_bytes());
19888        continuation_header_and_segment_1.extend_from_slice(payload_segment_1);
19889        peer.write_all(&continuation_header_and_segment_1)
19890            .expect("loopback write must complete");
19891        peer.flush().expect("loopback flush must complete");
19892
19893        // Drive reads until the connection has consumed exactly the first
19894        // segment and is waiting on the rest of THIS SAME CONTINUATION
19895        // frame's payload — a genuine partial `socket_read()`, proven by
19896        // `expect_read` still wanting the remainder.
19897        for _ in 0..64 {
19898            connection.readable(&mut context, EndpointClient(&mut router));
19899            if matches!(connection.core.state, H2State::ContinuationFrame(_))
19900                && matches!(
19901                    connection.core.stream_table.expect_read(),
19902                    Some((H2StreamId::Zero, remaining)) if remaining == payload_segment_2.len()
19903                )
19904            {
19905                break;
19906            }
19907            std::thread::yield_now();
19908        }
19909        assert!(
19910            matches!(connection.core.state, H2State::ContinuationFrame(_)),
19911            "the CONTINUATION frame must be mid-payload, waiting on its \
19912             second TCP segment: {:?}",
19913            connection.core.state
19914        );
19915        assert_eq!(
19916            connection.core.stream_table.expect_read(),
19917            Some((H2StreamId::Zero, payload_segment_2.len())),
19918            "expect_read must want exactly the remaining bytes of this \
19919             frame's own payload — proof this is a genuine partial read, \
19920             not an inter-frame gap"
19921        );
19922
19923        // Exactly B2's trigger: the connection is draining AND the frontend
19924        // readiness reports HUP, landing squarely mid-payload-read — the
19925        // shape a TCP half-close produces (mio's `is_read_closed()` can be
19926        // true with data the peer already sent still unread).
19927        connection.core.drain.__test_set_draining();
19928        connection.core.readiness.event.insert(Ready::HUP);
19929        connection.writable(&mut context, EndpointClient(&mut router));
19930        connection.core.readiness.event.remove(Ready::HUP);
19931
19932        // Now the second TCP segment, completing the CONTINUATION frame's
19933        // payload, arrives.
19934        peer.write_all(payload_segment_2)
19935            .expect("loopback write must complete");
19936        peer.flush().expect("loopback flush must complete");
19937
19938        for _ in 0..64 {
19939            connection.readable(&mut context, EndpointClient(&mut router));
19940            if !matches!(
19941                connection.core.state,
19942                H2State::ContinuationHeader(_) | H2State::ContinuationFrame(_)
19943            ) {
19944                break;
19945            }
19946            std::thread::yield_now();
19947        }
19948
19949        assert!(
19950            !matches!(connection.core.state, H2State::Error | H2State::GoAway),
19951            "a HUP-while-draining event landing mid-TCP-segment must not \
19952             corrupt a legitimate in-progress header block into a \
19953             connection error: {:?}",
19954            connection.core.state
19955        );
19956
19957        let next_block = peer_encoder.encode([(&b"x-sozu-marker"[..], &b"legitimate-value"[..])]);
19958        assert_eq!(
19959            next_block.len(),
19960            1,
19961            "the peer must now reference its dynamic entry, not re-send a literal"
19962        );
19963
19964        let mut decoded = Vec::new();
19965        let status = connection
19966            .core
19967            .hpack
19968            .decoder_mut()
19969            .decode_with_cb(&next_block, |k, v| {
19970                decoded.push((k.into_owned(), v.into_owned()));
19971            });
19972
19973        assert!(
19974            status.is_ok(),
19975            "the connection decoder must still resolve the peer's dynamic \
19976             table after a HUP-while-draining event landing mid-TCP-segment, \
19977             got {status:?}"
19978        );
19979        assert_eq!(
19980            decoded,
19981            vec![(b"x-sozu-marker".to_vec(), b"legitimate-value".to_vec())],
19982            "stream 1's field block must have been decoded byte-for-byte, \
19983             not clobbered by a HUP-while-draining event landing between \
19984             two TCP segments of the same CONTINUATION frame"
19985        );
19986    }
19987
19988    // ── The HPACK encoder across one multi-stream write pass ────────────
19989    //
19990    // `converter.rs`'s own unit tests drive ONE `H2BlockConverter` over ONE
19991    // kawa. Nothing there can see a property that only exists across the
19992    // streams of a single `write_streams` pass, and those are precisely the
19993    // two the per-`kawa.prepare` converter scoping has to preserve: the
19994    // RFC 7541 §6.3 size-update prefix belongs to the first header block of
19995    // the pass and to no other, and every block of the pass is encoded
19996    // against the SAME encoder dynamic table.
19997
19998    /// Split a captured H2 byte stream into its HEADERS frame payloads,
19999    /// each paired with the stream it belongs to, in wire order. DATA and
20000    /// control frames are skipped; a truncated capture is a test bug and
20001    /// asserts rather than silently returning a short list.
20002    fn headers_blocks(wire: &[u8]) -> Vec<(StreamId, Vec<u8>)> {
20003        let mut blocks = Vec::new();
20004        let mut offset = 0usize;
20005        while offset + parser::FRAME_HEADER_SIZE <= wire.len() {
20006            let payload_len = ((wire[offset] as usize) << 16)
20007                | ((wire[offset + 1] as usize) << 8)
20008                | wire[offset + 2] as usize;
20009            let frame_type = wire[offset + 3];
20010            let stream_id = u32::from_be_bytes([
20011                wire[offset + 5],
20012                wire[offset + 6],
20013                wire[offset + 7],
20014                wire[offset + 8],
20015            ]) & 0x7fff_ffff;
20016            let start = offset + parser::FRAME_HEADER_SIZE;
20017            let end = start + payload_len;
20018            assert!(
20019                end <= wire.len(),
20020                "captured frame must be complete: {payload_len} payload bytes \
20021                 declared at offset {offset} of a {} byte capture",
20022                wire.len()
20023            );
20024            if frame_type == 1 {
20025                blocks.push((stream_id, wire[start..end].to_vec()));
20026            }
20027            offset = end;
20028        }
20029        blocks
20030    }
20031
20032    /// `true` when `block` opens with an RFC 7541 §6.3 dynamic-table-size
20033    /// update. The representation prefixes partition the first byte with no
20034    /// overlap — `1xxxxxxx` indexed, `01xxxxxx` literal-with-indexing,
20035    /// `0001xxxx` literal-never-indexed, `0000xxxx` literal-without-indexing
20036    /// — so the `001xxxxx` pattern identifies the update on its own,
20037    /// whatever integer follows it.
20038    fn starts_with_size_update(block: &[u8]) -> bool {
20039        block.first().is_some_and(|b| b & 0xE0 == 0x20)
20040    }
20041
20042    /// Drive exactly ONE `write_streams` pass that emits a response header
20043    /// block on every `stream_ids` entry, and return those blocks in the
20044    /// order they reached the wire together with whatever size-update the
20045    /// connection still has queued afterwards.
20046    ///
20047    /// Each stream is opened by a real HEADERS frame from the peer and then
20048    /// filled with sozu's own 404 default answer through
20049    /// `answers::set_default_answer`, the same chokepoint the routing layer
20050    /// uses — so the blocks captured here are the ones production encodes.
20051    ///
20052    /// `pending_table_size_update` is installed the way `handle_settings_frame`
20053    /// installs it: the encoder cap and the queued signal move together.
20054    fn drive_one_write_pass(
20055        stream_ids: &[StreamId],
20056        pending_table_size_update: Option<u32>,
20057    ) -> (Vec<(StreamId, Vec<u8>)>, Option<u32>) {
20058        use std::io::{Read, Write};
20059
20060        let pool = Rc::new(RefCell::new(Pool::with_capacity(8, 40, 16_384)));
20061        let (mut connection, mut peer) = test_h2_connection(&pool, None);
20062        let mut context = test_context(&pool);
20063        let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
20064
20065        connection.core.state = H2State::Header;
20066        connection
20067            .core
20068            .stream_table
20069            .set_expect_read(Some((H2StreamId::Zero, 9)));
20070
20071        let mut peer_encoder = crate::protocol::mux::hpack::Encoder::new();
20072        for &stream_id in stream_ids {
20073            let field_block = peer_encoder.encode([
20074                (&b":method"[..], &b"GET"[..]),
20075                (&b":scheme"[..], &b"https"[..]),
20076                (&b":authority"[..], &b"example.com"[..]),
20077                (&b":path"[..], &b"/write-pass"[..]),
20078            ]);
20079            let mut frame = Vec::with_capacity(parser::FRAME_HEADER_SIZE + field_block.len());
20080            frame.extend_from_slice(&(field_block.len() as u32).to_be_bytes()[1..]);
20081            frame.push(1); // HEADERS
20082            frame.push(parser::FLAG_END_STREAM | parser::FLAG_END_HEADERS);
20083            frame.extend_from_slice(&stream_id.to_be_bytes());
20084            frame.extend_from_slice(&field_block);
20085            peer.write_all(&frame)
20086                .expect("loopback write must complete");
20087            peer.flush().expect("loopback flush must complete");
20088            for _ in 0..64 {
20089                connection.readable(&mut context, EndpointClient(&mut router));
20090                if connection
20091                    .core
20092                    .stream_table
20093                    .streams()
20094                    .contains_key(&stream_id)
20095                {
20096                    break;
20097                }
20098                std::thread::yield_now();
20099            }
20100            assert!(
20101                connection
20102                    .core
20103                    .stream_table
20104                    .streams()
20105                    .contains_key(&stream_id),
20106                "stream {stream_id} must be accepted before the write pass"
20107            );
20108        }
20109
20110        let answers = context.listener.borrow().get_answers().clone();
20111        let open: Vec<GlobalStreamId> = connection
20112            .core
20113            .stream_table
20114            .streams()
20115            .values()
20116            .copied()
20117            .collect();
20118        for global_stream_id in open {
20119            crate::protocol::mux::answers::set_default_answer(
20120                &mut context.streams[global_stream_id],
20121                &mut connection.core.readiness,
20122                404,
20123                &answers.borrow(),
20124            );
20125        }
20126
20127        // Discard whatever the handshake already put on the wire, so the
20128        // capture below holds this write pass and nothing else.
20129        let mut discard = Vec::new();
20130        let _ = peer.read_to_end(&mut discard);
20131
20132        if let Some(size) = pending_table_size_update {
20133            connection
20134                .core
20135                .hpack
20136                .set_encoder_max_table_size(size as usize);
20137        }
20138        connection.core.pending_table_size_update = pending_table_size_update;
20139        connection.writable(&mut context, EndpointClient(&mut router));
20140
20141        let mut wire = Vec::new();
20142        let _ = peer.read_to_end(&mut wire);
20143        (
20144            headers_blocks(&wire),
20145            connection.core.pending_table_size_update,
20146        )
20147    }
20148
20149    /// Two properties of a `write_streams` pass, both invisible to any
20150    /// single-stream test, pinned on the bytes two streams actually receive:
20151    ///
20152    /// 1. the RFC 7541 §6.3 dynamic-table-size-update prefix is written to
20153    ///    the FIRST header block of the pass and to no other, and the
20154    ///    connection's mirror is cleared only because a block carried it;
20155    /// 2. both blocks are encoded against ONE encoder dynamic table, so a
20156    ///    peer replaying them in order with a single decoder stays in sync —
20157    ///    and a decoder that never saw the first block CANNOT read the
20158    ///    second, which is the negative half that makes assertion 2
20159    ///    discriminating rather than vacuous.
20160    ///
20161    /// Scoping the `H2BlockConverter` to a single `kawa.prepare` call is
20162    /// what puts both at risk: the converter no longer spans the per-stream
20163    /// loop, so the signal and the encoder now cross from one stream to the
20164    /// next through [`converter::H2ConverterPass`] rather than through one
20165    /// long-lived struct.
20166    ///
20167    /// TO SEE THIS RED, break either half:
20168    /// (a) delete `self.pending_table_size_update = converter.pending_table_size_update;`
20169    ///     from `converter::H2ConverterPass::reclaim` — every block of the
20170    ///     pass then re-emits the prefix, and the second assertion fails with
20171    ///     `only the FIRST header block of a pass carries the size update;
20172    ///     stream 3 opened with 0x3f`;
20173    /// (b) pass a fresh `crate::protocol::mux::hpack::Encoder::new()` to `pass.converter(..)`
20174    ///     in `write_streams` instead of `self.hpack.encoder_mut()` — every
20175    ///     block becomes self-contained and the LAST assertion fails with
20176    ///     `the second block must depend on the dynamic table the first one
20177    ///     built`.
20178    #[test]
20179    fn one_write_pass_prefixes_its_first_header_block_only_and_shares_one_encoder() {
20180        // 4096 is the HPACK default table size, so `[0x3f, 0xe1, 0x1f]` below
20181        // is the canonical §6.3 encoding: prefix bits `001`, a 5-bit prefix
20182        // integer saturated to 31, then 4096 - 31 = 4065 as two continuation
20183        // octets (0x61 | 0x80, 0x1f). Written out rather than produced with
20184        // the same encoder the production path uses, so the test cannot
20185        // agree with a broken encoder.
20186        const TABLE_SIZE: u32 = 4096;
20187        const SIZE_UPDATE_PREFIX: [u8; 3] = [0x3f, 0xe1, 0x1f];
20188
20189        let (blocks, residual) = drive_one_write_pass(&[1, 3], Some(TABLE_SIZE));
20190
20191        assert_eq!(
20192            blocks.iter().map(|(id, _)| *id).collect::<Vec<_>>(),
20193            vec![1, 3],
20194            "one pass must emit one header block per open stream, in the \
20195             priority order (equal urgency, ascending stream id)"
20196        );
20197
20198        assert_eq!(
20199            &blocks[0].1[..SIZE_UPDATE_PREFIX.len()],
20200            &SIZE_UPDATE_PREFIX,
20201            "the first header block of the pass must open with the \
20202             dynamic-table-size-update for {TABLE_SIZE}"
20203        );
20204        assert!(
20205            !starts_with_size_update(&blocks[1].1),
20206            "only the FIRST header block of a pass carries the size update; \
20207             stream {} opened with {:#04x}",
20208            blocks[1].0,
20209            blocks[1].1[0]
20210        );
20211        assert_eq!(
20212            residual, None,
20213            "the connection clears its mirror once a block carried the signal"
20214        );
20215
20216        // Positive half: the peer's single decoder replays the pass in order.
20217        let mut peer_decoder = crate::protocol::mux::hpack::Decoder::new();
20218        peer_decoder.set_max_allowed_table_size(TABLE_SIZE as usize);
20219        for (stream_id, block) in &blocks {
20220            let mut decoded = Vec::new();
20221            let status = peer_decoder.decode_with_cb(block, |key, value| {
20222                decoded.push((key.into_owned(), value.into_owned()));
20223            });
20224            assert!(
20225                status.is_ok(),
20226                "a peer replaying the pass in order must decode stream \
20227                 {stream_id}'s block: {status:?}"
20228            );
20229            assert!(
20230                decoded.contains(&(b":status".to_vec(), b"404".to_vec())),
20231                "stream {stream_id}'s block must carry the 404 status line, \
20232                 got {decoded:?}"
20233            );
20234        }
20235
20236        // Negative half: the second block is not self-contained. It indexes
20237        // dynamic-table entries only the first block created, so a decoder
20238        // that never saw the first block cannot read it. Without this, a
20239        // per-stream encoder would satisfy every assertion above.
20240        let mut fresh_decoder = crate::protocol::mux::hpack::Decoder::new();
20241        fresh_decoder.set_max_allowed_table_size(TABLE_SIZE as usize);
20242        let status = fresh_decoder.decode_with_cb(&blocks[1].1, |_, _| {});
20243        assert!(
20244            status.is_err(),
20245            "the second block must depend on the dynamic table the first one \
20246             built — a decoder that never saw the first block must fail on \
20247             it, otherwise the two blocks came from two encoders"
20248        );
20249    }
20250
20251    // ── RFC 9113 §3.4 client connection preface, read-fragmentation axis ──
20252    //
20253    // `Connection::new_h2_server` arms `expect_read` with
20254    // `CLIENT_PREFACE_SIZE` (the 24-octet magic string plus a 9-octet
20255    // SETTINGS frame header). When a read comes back SHORT of that request,
20256    // the short-read branch of `H2Shell::readable` runs an early guard
20257    // so a client that is plainly not speaking H2 is dropped without waiting
20258    // for the rest of the window.
20259    //
20260    // Nothing about a TCP segment or a TLS record guarantees it lands on the
20261    // 24-octet boundary, so that guard has to tolerate EVERY split of a
20262    // byte-perfect preface while still refusing a wrong one.
20263
20264    /// The whole accumulated window, at every chunk size, is the axis that
20265    /// matters here — not one hand-picked split. The failure this pins is
20266    /// invisible at exactly 24 (the window never grows past the magic
20267    /// string, so a prefix test against it still answers correctly) and at
20268    /// exactly 33 (the request is filled, so the short-read branch is never
20269    /// entered at all) — the two sizes a hand-written test is most likely to
20270    /// pick. So the sweep collects every failing size instead of asserting
20271    /// inside the loop, which would stop at the first one and hide the band.
20272    ///
20273    /// To SEE THIS RED: in the `H2State::ClientPreface` arm of
20274    /// `H2Shell::readable`'s short-read branch, compare the whole
20275    /// accumulated window instead of the part the constant can cover —
20276    /// `if !pri.starts_with(i)` in place of
20277    /// `if !pri.starts_with(&i[..i.len().min(pri.len())])`. `starts_with`
20278    /// asks whether the window is a prefix OF the 24-octet constant, so it
20279    /// answers false for every window longer than 24 whatever the window
20280    /// holds, and this reports `[1, 25, 26, 27, 28, 29, 30, 31, 32]`.
20281    #[test]
20282    fn a_byte_perfect_client_preface_survives_every_read_fragmentation() {
20283        use std::io::Write;
20284
20285        // Built from the wire bytes, not from `serializer::H2_PRI`, so this
20286        // also pins the constant's value rather than agreeing with whatever
20287        // it happens to hold.
20288        let mut wire = Vec::with_capacity(CLIENT_PREFACE_SIZE);
20289        wire.extend_from_slice(b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n");
20290        // SETTINGS, empty payload, no flags, stream 0 — RFC 9113 §6.5.
20291        wire.extend_from_slice(&[0, 0, 0, 4, 0, 0, 0, 0, 0]);
20292        assert_eq!(
20293            wire.len(),
20294            CLIENT_PREFACE_SIZE,
20295            "the probe must be exactly the window the server asks for"
20296        );
20297
20298        let mut disconnected = Vec::new();
20299
20300        for chunk in 1..=40usize {
20301            let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16_384)));
20302            let (mut connection, mut peer) = test_h2_connection(&pool, None);
20303            let mut context = test_context(&pool);
20304            let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
20305
20306            let mut delivered = 0usize;
20307            let mut closed = false;
20308
20309            'delivery: for piece in wire.chunks(chunk) {
20310                peer.write_all(piece).expect("loopback write must complete");
20311                peer.flush().expect("loopback flush must complete");
20312                delivered += piece.len();
20313
20314                // Drive until THIS piece has been consumed before writing the
20315                // next one. Two loopback writes the connection has not read
20316                // between coalesce in the kernel receive queue, which would
20317                // hand `socket_read` a full 33-octet window, take the
20318                // not-short path, and never reach the guard under test.
20319                for _ in 0..64 {
20320                    if !matches!(connection.core.state, H2State::ClientPreface) {
20321                        break 'delivery;
20322                    }
20323                    if matches!(
20324                        connection.readable(&mut context, EndpointClient(&mut router)),
20325                        MuxResult::CloseSession
20326                    ) {
20327                        closed = true;
20328                        break 'delivery;
20329                    }
20330                    if connection.core.stream_table.expect_read()
20331                        == Some((H2StreamId::Zero, CLIENT_PREFACE_SIZE - delivered))
20332                    {
20333                        break;
20334                    }
20335                    std::thread::yield_now();
20336                }
20337            }
20338
20339            if closed
20340                || matches!(
20341                    connection.core.state,
20342                    H2State::ClientPreface | H2State::Error | H2State::GoAway
20343                )
20344            {
20345                disconnected.push(chunk);
20346            }
20347        }
20348
20349        assert!(
20350            disconnected.is_empty(),
20351            "a byte-perfect client preface must be accepted however the \
20352             reads are fragmented; these chunk sizes were disconnected \
20353             instead: {disconnected:?}"
20354        );
20355    }
20356
20357    /// The other half of the same guard: clipping the comparison to the part
20358    /// the magic string can cover must not blunt it. A window that is wrong
20359    /// INSIDE the first 24 octets is still refused while the request is
20360    /// unfilled — which is the only moment the early guard can act, since
20361    /// `parser::preface` in the `H2State::ClientPreface` arm is reached only
20362    /// once the whole `CLIENT_PREFACE_SIZE` window has been read.
20363    ///
20364    /// Corrupting the FIRST and the LAST octet of the magic string proves
20365    /// the comparison still spans all 24, and sweeping the window across the
20366    /// 25..=32 band proves the fix did not turn the band into a hole that
20367    /// lets a non-H2 client linger.
20368    ///
20369    /// To SEE THIS RED: drop the guard's `!`, or clip the comparison to
20370    /// fewer octets than the magic string holds — e.g.
20371    /// `&i[..i.len().min(pri.len() - 1)]`, which stops refusing the
20372    /// `corrupt_at = 23` cases.
20373    #[test]
20374    fn an_invalid_client_preface_is_still_refused_before_the_window_is_filled() {
20375        use std::io::Write;
20376
20377        let mut survivors = Vec::new();
20378
20379        for corrupt_at in [0usize, 23] {
20380            for window in (corrupt_at + 1)..CLIENT_PREFACE_SIZE {
20381                let mut wire = Vec::with_capacity(CLIENT_PREFACE_SIZE);
20382                wire.extend_from_slice(b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n");
20383                wire.extend_from_slice(&[0, 0, 0, 4, 0, 0, 0, 0, 0]);
20384                wire[corrupt_at] ^= 0xff;
20385                wire.truncate(window);
20386
20387                let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16_384)));
20388                let (mut connection, mut peer) = test_h2_connection(&pool, None);
20389                let mut context = test_context(&pool);
20390                let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
20391
20392                peer.write_all(&wire).expect("loopback write must complete");
20393                peer.flush().expect("loopback flush must complete");
20394
20395                // The window is short of `CLIENT_PREFACE_SIZE`, so the
20396                // connection never leaves `H2State::ClientPreface` on its
20397                // own: whatever refuses it here is the early guard.
20398                for _ in 0..64 {
20399                    if matches!(
20400                        connection.readable(&mut context, EndpointClient(&mut router)),
20401                        MuxResult::CloseSession
20402                    ) {
20403                        break;
20404                    }
20405                    std::thread::yield_now();
20406                }
20407
20408                if !matches!(connection.core.state, H2State::Error) {
20409                    survivors.push((corrupt_at, window));
20410                }
20411            }
20412        }
20413
20414        assert!(
20415            survivors.is_empty(),
20416            "a preface wrong inside its first 24 octets must be refused \
20417             before the request is filled; these (corrupt_at, window) pairs \
20418             survived: {survivors:?}"
20419        );
20420    }
20421
20422    // ── Property coverage: the HPACK encoder across a multi-stream pass ──
20423    //
20424    // The deterministic test above fixes ONE shape: two streams, the HPACK
20425    // default table size, the size update landing on the first of exactly
20426    // two blocks. This property generalises the stream count (2..=5) and the
20427    // advertised table size, which decides both how many octets the §6.3
20428    // prefix occupies and how much dynamic table the later blocks can index.
20429    //
20430    // It is a sibling of `reassembly_property` below rather than a case
20431    // inside it: that one drives the READ path and its oracle is "one
20432    // decoder stays in sync across a fragmented header block", while this
20433    // one drives the WRITE path and its oracle is "one encoder, one prefix,
20434    // N blocks a single peer decoder replays". One `quickcheck` verdict over
20435    // two unrelated state machines would say nothing about either.
20436    //
20437    // The negative half of the deterministic test — a fresh decoder must
20438    // FAIL on a later block — is deliberately left out here: a generated
20439    // table size small enough to evict the dynamic table makes every block
20440    // self-contained, which is correct behaviour and would fail that
20441    // assertion.
20442    mod write_pass_property {
20443        use quickcheck::{Arbitrary, Gen, TestResult, quickcheck};
20444
20445        use super::*;
20446
20447        /// One generated write pass: how many streams share it, and the
20448        /// `SETTINGS_HEADER_TABLE_SIZE` the peer advertised just before it.
20449        #[derive(Debug, Clone, Copy)]
20450        struct WritePassPlan {
20451            streams: u8,
20452            table_size: u16,
20453        }
20454
20455        impl Arbitrary for WritePassPlan {
20456            fn arbitrary(g: &mut Gen) -> Self {
20457                WritePassPlan {
20458                    streams: *g.choose(&[2u8, 3, 4, 5]).expect("non-empty slice"),
20459                    // Bounded by the HPACK default a fresh `Decoder`
20460                    // accepts, mirrored onto the peer decoder below; a
20461                    // larger advertisement is a SETTINGS-path concern, not
20462                    // a write-pass one.
20463                    table_size: u16::arbitrary(g) % 4097,
20464                }
20465            }
20466        }
20467
20468        fn drive(plan: WritePassPlan) -> TestResult {
20469            let stream_ids: Vec<StreamId> =
20470                (0..u32::from(plan.streams)).map(|i| 1 + 2 * i).collect();
20471            let (blocks, residual) =
20472                drive_one_write_pass(&stream_ids, Some(u32::from(plan.table_size)));
20473
20474            if blocks
20475                .iter()
20476                .map(|(id, _)| *id)
20477                .ne(stream_ids.iter().copied())
20478            {
20479                return TestResult::error(format!(
20480                    "plan {plan:?}: expected one block per stream in priority \
20481                     order {stream_ids:?}, got {:?}",
20482                    blocks.iter().map(|(id, _)| *id).collect::<Vec<_>>()
20483                ));
20484            }
20485            if !starts_with_size_update(&blocks[0].1) {
20486                return TestResult::error(format!(
20487                    "plan {plan:?}: the first block must open with the \
20488                     size update, got {:#04x}",
20489                    blocks[0].1[0]
20490                ));
20491            }
20492            for (stream_id, block) in blocks.iter().skip(1) {
20493                if starts_with_size_update(block) {
20494                    return TestResult::error(format!(
20495                        "plan {plan:?}: stream {stream_id} re-emitted the \
20496                         size update the first block already carried"
20497                    ));
20498                }
20499            }
20500            if residual.is_some() {
20501                return TestResult::error(format!(
20502                    "plan {plan:?}: the mirror must be cleared once a block \
20503                     carried the signal, still {residual:?}"
20504                ));
20505            }
20506
20507            let mut peer_decoder = crate::protocol::mux::hpack::Decoder::new();
20508            peer_decoder.set_max_allowed_table_size(usize::from(plan.table_size));
20509            for (stream_id, block) in &blocks {
20510                let mut decoded = Vec::new();
20511                let status = peer_decoder.decode_with_cb(block, |key, value| {
20512                    decoded.push((key.into_owned(), value.into_owned()));
20513                });
20514                if status.is_err() {
20515                    return TestResult::error(format!(
20516                        "plan {plan:?}: a peer replaying the pass in order \
20517                         desynced on stream {stream_id}: {status:?}"
20518                    ));
20519                }
20520                if !decoded.contains(&(b":status".to_vec(), b"404".to_vec())) {
20521                    return TestResult::error(format!(
20522                        "plan {plan:?}: stream {stream_id} decoded to \
20523                         {decoded:?}, expected the 404 status line"
20524                    ));
20525                }
20526            }
20527            TestResult::passed()
20528        }
20529
20530        quickcheck! {
20531            fn qc_one_write_pass_prefixes_its_first_header_block_only(plan: WritePassPlan) -> TestResult {
20532                drive(plan)
20533            }
20534        }
20535    }
20536
20537    // ── Property coverage: interleaved reassembly + control-frame flushes ──
20538    //
20539    // The four deterministic pinning tests above each fix ONE interleaving
20540    // (an unrelated WINDOW_UPDATE flush, a graceful_goaway, a HUP-while-
20541    // draining event between frames, a HUP-while-draining event mid-TCP-
20542    // segment) at ONE point. This property generalises them: a
20543    // `quickcheck`-driven `ReassemblyPlan` splits a real HPACK field block
20544    // into 2..=5 CONTINUATION fragments and independently chooses, before
20545    // each one, whether to interleave nothing, an unrelated WINDOW_UPDATE
20546    // flush, a `graceful_goaway`, or a HUP-while-draining event — the same
20547    // four triggers, now composed in every order and count `quickcheck`
20548    // cares to generate — and, independently again, whether that interleave
20549    // lands BETWEEN two frames or in the MIDDLE of one frame's own TCP-level
20550    // write (review of `e1c3c2fb`, B2/B3: the two shapes are not
20551    // interchangeable — `zero.storage` is empty in the former and holds a
20552    // genuine partial `socket_read()` in the latter, and only the latter
20553    // reaches the narrower guard the four deterministic tests individually
20554    // pin). `quickcheck` is already a workspace dependency (see
20555    // `lib/src/router/mod.rs`'s `qc_router_hostname_resolution_matches_
20556    // the_documented_semantics`, whose `Arbitrary`/`quickcheck!` shape this
20557    // mirrors).
20558    //
20559    // Any out-of-bounds read or other panic inside `drive()` fails the
20560    // `quickcheck` run directly — there is no separate assertion for it.
20561    // CONTINUATION-flood abort and the CVE-2024-27316 refusal path are
20562    // deliberately NOT folded into this property: they end the block in a
20563    // *different* terminal state (refused, not decoded), which would split
20564    // this property's single pass/fail oracle in two. That path already has
20565    // dedicated deterministic coverage —
20566    // `a_refused_stream_keeps_the_hpack_decoder_in_sync` and
20567    // `a_refused_padded_prioritized_stream_keeps_the_hpack_decoder_in_sync`
20568    // above.
20569    mod reassembly_property {
20570        use quickcheck::{Arbitrary, Gen, TestResult, quickcheck};
20571
20572        use super::*;
20573
20574        /// One interleaved event the property driver may inject before a
20575        /// CONTINUATION fragment. Mirrors the four deterministic pinning
20576        /// tests' triggers.
20577        #[derive(Debug, Clone, Copy)]
20578        enum Interleave {
20579            None,
20580            UnrelatedWindowUpdateFlush,
20581            HupWhileDraining,
20582            GracefulGoaway,
20583        }
20584
20585        impl Arbitrary for Interleave {
20586            fn arbitrary(g: &mut Gen) -> Self {
20587                *g.choose(&[
20588                    Interleave::None,
20589                    Interleave::UnrelatedWindowUpdateFlush,
20590                    Interleave::HupWhileDraining,
20591                    Interleave::GracefulGoaway,
20592                ])
20593                .expect("the choice slice above is non-empty")
20594            }
20595        }
20596
20597        /// One interleave point, paired with whether it lands between two
20598        /// frames (`mid_frame == false`, the original shape) or in the
20599        /// middle of the following fragment's own TCP-level write
20600        /// (`mid_frame == true` — a genuine partial `socket_read()`, the
20601        /// shape B2 proved is NOT interchangeable with the former).
20602        #[derive(Debug, Clone, Copy)]
20603        struct InterleavePoint {
20604            interleave: Interleave,
20605            mid_frame: bool,
20606        }
20607
20608        impl Arbitrary for InterleavePoint {
20609            fn arbitrary(g: &mut Gen) -> Self {
20610                InterleavePoint {
20611                    interleave: Interleave::arbitrary(g),
20612                    mid_frame: bool::arbitrary(g),
20613                }
20614            }
20615        }
20616
20617        /// How many CONTINUATION frames to split the field block into
20618        /// (2..=5) and which [`InterleavePoint`] to inject before each one
20619        /// after the first — exactly `splits - 1` points, one per fragment
20620        /// boundary, never `splits` (there is no point "before the first
20621        /// fragment": the HEADERS frame that carries it is what starts the
20622        /// reassembly).
20623        #[derive(Debug, Clone)]
20624        struct ReassemblyPlan {
20625            splits: u8,
20626            points: Vec<InterleavePoint>,
20627        }
20628
20629        impl Arbitrary for ReassemblyPlan {
20630            fn arbitrary(g: &mut Gen) -> Self {
20631                let splits = 2 + (u8::arbitrary(g) % 4); // 2..=5
20632                let points = (0..splits - 1)
20633                    .map(|_| InterleavePoint::arbitrary(g))
20634                    .collect();
20635                ReassemblyPlan { splits, points }
20636            }
20637        }
20638
20639        /// Apply one interleave. `GracefulGoaway` checks
20640        /// `connection.drain.draining()` — the AUTHORITATIVE state, not a
20641        /// separately tracked bool — before calling `graceful_goaway`:
20642        /// once the connection is already draining (whether a prior point
20643        /// in this same plan called `graceful_goaway` itself, OR a prior
20644        /// `HupWhileDraining` point set it via the test-only
20645        /// `__test_set_draining()` backdoor), a SECOND `graceful_goaway`
20646        /// call would be the RFC 9113 §6.8 FINAL GOAWAY
20647        /// (`GracefulDrainDecision::AlreadyDraining`), which drops
20648        /// `expect_read` and makes the block permanently uncompletable — a
20649        /// different scenario than this property drives, so a
20650        /// `GracefulGoaway` point reached while already draining is
20651        /// treated as `None` instead. A local `bool` tracking only
20652        /// "did GracefulGoaway itself fire" would miss the
20653        /// `HupWhileDraining` case and was exactly the bug `quickcheck`
20654        /// found while writing this property (review of `e1c3c2fb`, B3).
20655        fn apply_interleave<L>(
20656            connection: &mut H2Shell<mio::net::TcpStream>,
20657            context: &mut Context<L>,
20658            router: &mut Router,
20659            interleave: Interleave,
20660        ) where
20661            L: ListenerHandler + L7ListenerHandler,
20662        {
20663            match interleave {
20664                Interleave::None => {}
20665                Interleave::UnrelatedWindowUpdateFlush => {
20666                    connection.core.queue_window_update(0, 65_535);
20667                    connection.writable(context, EndpointClient(router));
20668                }
20669                Interleave::HupWhileDraining => {
20670                    connection.core.drain.__test_set_draining();
20671                    connection.core.readiness.event.insert(Ready::HUP);
20672                    connection.writable(context, EndpointClient(router));
20673                    connection.core.readiness.event.remove(Ready::HUP);
20674                }
20675                Interleave::GracefulGoaway => {
20676                    if !connection.core.drain.draining() {
20677                        let now = connection.core.now;
20678                        connection.graceful_goaway(now);
20679                    }
20680                }
20681            }
20682        }
20683
20684        /// Build the connection, split a real field block into
20685        /// `plan.splits` fragments, and send them as HEADERS + N-1
20686        /// CONTINUATION frames, injecting `plan.points[i - 1]` before
20687        /// fragment `i` (`i >= 1`) — either between the two frames, or
20688        /// split across two TCP-level writes of that fragment's OWN
20689        /// payload with the interleave firing in between. Asserts the
20690        /// connection stays usable and the block decodes byte-for-byte
20691        /// regardless of which interleaves fired or where.
20692        fn drive(plan: ReassemblyPlan) -> TestResult {
20693            use std::io::Write;
20694
20695            let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 20, 16_384)));
20696            let (mut connection, mut peer) = test_h2_connection(&pool, None);
20697            // The interleaved WINDOW_UPDATE flush really writes to the socket
20698            // since #1604 (it used to be deferred while a block was being
20699            // reassembled). Once sozu's end sends data, Linux switches it to
20700            // delayed ACKs ("pingpong" mode), so the peer's last segment is
20701            // acknowledged up to 40 ms late, and the peer's Nagle holds its
20702            // next small CONTINUATION until that ACK: longer than the
20703            // yield-bounded read loops below wait. H2 clients disable Nagle.
20704            peer.set_nodelay(true)
20705                .expect("the loopback peer must accept TCP_NODELAY");
20706            let mut context = test_context(&pool);
20707            let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
20708
20709            connection.core.state = H2State::Header;
20710            connection
20711                .core
20712                .stream_table
20713                .set_expect_read(Some((H2StreamId::Zero, 9)));
20714
20715            let mut peer_encoder = crate::protocol::mux::hpack::Encoder::new();
20716            let field_block = peer_encoder.encode([
20717                (&b":method"[..], &b"GET"[..]),
20718                (&b":scheme"[..], &b"https"[..]),
20719                (&b":authority"[..], &b"example.com"[..]),
20720                (&b":path"[..], &b"/qc-multiframe"[..]),
20721                (&b"x-sozu-qc-marker"[..], &b"qc-value"[..]),
20722            ]);
20723            let splits = plan.splits as usize;
20724            if field_block.len() < splits * 2 {
20725                // Too few bytes to split `splits` ways meaningfully.
20726                return TestResult::discard();
20727            }
20728
20729            let chunk = (field_block.len() / splits).max(1);
20730            let mut fragments: Vec<&[u8]> = Vec::with_capacity(splits);
20731            let mut rest = &field_block[..];
20732            for i in 0..splits {
20733                if i + 1 == splits {
20734                    fragments.push(rest);
20735                } else {
20736                    let (a, b) = rest.split_at(chunk);
20737                    fragments.push(a);
20738                    rest = b;
20739                }
20740            }
20741            if fragments.iter().any(|f| f.is_empty()) {
20742                return TestResult::discard();
20743            }
20744
20745            let first = fragments[0];
20746            let mut frame = Vec::with_capacity(9 + first.len());
20747            frame.extend_from_slice(&(first.len() as u32).to_be_bytes()[1..]);
20748            frame.push(1); // HEADERS
20749            frame.push(parser::FLAG_END_STREAM);
20750            frame.extend_from_slice(&1u32.to_be_bytes());
20751            frame.extend_from_slice(first);
20752            if peer.write_all(&frame).is_err() || peer.flush().is_err() {
20753                return TestResult::discard();
20754            }
20755
20756            for _ in 0..64 {
20757                connection.readable(&mut context, EndpointClient(&mut router));
20758                if matches!(connection.core.state, H2State::ContinuationHeader(_)) {
20759                    break;
20760                }
20761                std::thread::yield_now();
20762            }
20763            if !matches!(connection.core.state, H2State::ContinuationHeader(_)) {
20764                return TestResult::discard();
20765            }
20766
20767            for (idx, fragment) in fragments.iter().enumerate().skip(1) {
20768                let point = plan.points[idx - 1];
20769                let is_last = idx + 1 == fragments.len();
20770                let end_headers_flag = if is_last { parser::FLAG_END_HEADERS } else { 0 };
20771
20772                if point.mid_frame && fragment.len() >= 2 {
20773                    // Split THIS fragment's own payload into two TCP-level
20774                    // writes, firing the interleave in between — a genuine
20775                    // partial `socket_read()`, not a gap between frames.
20776                    let mid = fragment.len() / 2;
20777                    let (segment_1, segment_2) = fragment.split_at(mid);
20778
20779                    let mut header_and_segment_1 = Vec::with_capacity(9 + segment_1.len());
20780                    header_and_segment_1
20781                        .extend_from_slice(&(fragment.len() as u32).to_be_bytes()[1..]);
20782                    header_and_segment_1.push(9); // CONTINUATION
20783                    header_and_segment_1.push(end_headers_flag);
20784                    header_and_segment_1.extend_from_slice(&1u32.to_be_bytes());
20785                    header_and_segment_1.extend_from_slice(segment_1);
20786                    if peer.write_all(&header_and_segment_1).is_err() || peer.flush().is_err() {
20787                        return TestResult::discard();
20788                    }
20789
20790                    for _ in 0..64 {
20791                        connection.readable(&mut context, EndpointClient(&mut router));
20792                        if matches!(connection.core.state, H2State::ContinuationFrame(_))
20793                            && matches!(
20794                                connection.core.stream_table.expect_read(),
20795                                Some((H2StreamId::Zero, remaining)) if remaining == segment_2.len()
20796                            )
20797                        {
20798                            break;
20799                        }
20800                        std::thread::yield_now();
20801                    }
20802                    if !matches!(connection.core.state, H2State::ContinuationFrame(_)) {
20803                        // The segment split did not land as a genuine
20804                        // partial read (e.g. too small to observe) — treat
20805                        // as a discard rather than a false failure.
20806                        return TestResult::discard();
20807                    }
20808
20809                    apply_interleave(&mut connection, &mut context, &mut router, point.interleave);
20810
20811                    if peer.write_all(segment_2).is_err() || peer.flush().is_err() {
20812                        return TestResult::discard();
20813                    }
20814                } else {
20815                    apply_interleave(&mut connection, &mut context, &mut router, point.interleave);
20816
20817                    let mut cframe = Vec::with_capacity(9 + fragment.len());
20818                    cframe.extend_from_slice(&(fragment.len() as u32).to_be_bytes()[1..]);
20819                    cframe.push(9); // CONTINUATION
20820                    cframe.push(end_headers_flag);
20821                    cframe.extend_from_slice(&1u32.to_be_bytes());
20822                    cframe.extend_from_slice(fragment);
20823                    if peer.write_all(&cframe).is_err() || peer.flush().is_err() {
20824                        return TestResult::discard();
20825                    }
20826                }
20827
20828                for _ in 0..64 {
20829                    connection.readable(&mut context, EndpointClient(&mut router));
20830                    if !matches!(
20831                        connection.core.state,
20832                        H2State::ContinuationHeader(_) | H2State::ContinuationFrame(_)
20833                    ) {
20834                        break;
20835                    }
20836                    std::thread::yield_now();
20837                }
20838            }
20839
20840            if matches!(connection.core.state, H2State::Error | H2State::GoAway) {
20841                return TestResult::error(format!(
20842                    "connection reached {:?} while reassembling under plan {plan:?}",
20843                    connection.core.state
20844                ));
20845            }
20846
20847            let next_block = peer_encoder.encode([(&b"x-sozu-qc-marker"[..], &b"qc-value"[..])]);
20848            let mut decoded = Vec::new();
20849            let status = connection
20850                .core
20851                .hpack
20852                .decoder_mut()
20853                .decode_with_cb(&next_block, |k, v| {
20854                    decoded.push((k.into_owned(), v.into_owned()));
20855                });
20856            if status.is_err() {
20857                return TestResult::error(format!(
20858                    "decoder desynced after plan {plan:?}: {status:?}"
20859                ));
20860            }
20861            if decoded != vec![(b"x-sozu-qc-marker".to_vec(), b"qc-value".to_vec())] {
20862                return TestResult::error(format!(
20863                    "decoded {decoded:?} after plan {plan:?}, expected the qc marker header"
20864                ));
20865            }
20866
20867            TestResult::passed()
20868        }
20869
20870        quickcheck! {
20871            fn qc_h2_header_reassembly_survives_interleaved_control_frame_flushes(plan: ReassemblyPlan) -> TestResult {
20872                drive(plan)
20873            }
20874        }
20875    }
20876
20877    // ── #1454: the write path over a REAL `FrontRustls` ─────────────────
20878    //
20879    // Every `socket_wants_write()` branch exercised above this point is
20880    // exercised over `BackpressuredTlsSocket` — a TCP handler with a modelled
20881    // answer. #1454 rules that shape out for closing it: `FrontRustls` is the
20882    // only production handler that can answer `true`, and a fixture that fakes
20883    // the answer on a TCP handler tests the fake. These are the first tests in
20884    // the tree that build a `H2Shell<FrontRustls>`;
20885    // `git grep -cE 'H2Shell<FrontRustls>' -- '*.rs'` answered 0 on every
20886    // commit before this one, including on the `write_streams` inversion's head.
20887    //
20888    // What only the real handler can settle: `(size > 0, WouldBlock)` is not a
20889    // scripted pair here. It is what `FrontRustls::socket_write_vectored`
20890    // returns when rustls absorbs plaintext into its own record buffer while
20891    // `write_tls` blocks against a full kernel send queue — `buffered_size` and
20892    // `can_write` are two independent quantities sharing one return value.
20893    // `update_readiness` classifies a pass as stalled iff `size == 0`, so that
20894    // pair is NOT a stall and the flush loop must go round again. Everything
20895    // written against a scripted pair assumes the production handler produces
20896    // it; nothing proved it until these tests.
20897    //
20898    // The handshake runs in memory between a real `rustls::ServerConnection`
20899    // and a real `rustls::ClientConnection`, and only then is the settled
20900    // server session attached to a loopback socket whose kernel queues are
20901    // pinned small. That order matters: a flight never has to fit through the
20902    // shrunken queue, so the backpressure under test is entirely the test's
20903    // and never the handshake's.
20904
20905    use crate::socket::FrontRustls;
20906
20907    /// Accepts whatever certificate the frontend serves.
20908    ///
20909    /// `lib/assets/certificate.pem` is self-signed and carries NO
20910    /// subjectAltName, so rustls's own web-PKI verifier rejects it whatever
20911    /// root store it is given — a CN-only certificate is not verifiable under
20912    /// RFC 6125 §6.4.4 as rustls implements it. The client here is a
20913    /// decryption oracle for bytes the frontend wrote, not a party whose trust
20914    /// decision is under test, so it asserts validity outright. Same shape as
20915    /// `e2e`'s `Verifier`, which exists for the same certificate.
20916    #[derive(Debug)]
20917    struct AcceptAnyServerCertificate;
20918
20919    impl rustls::client::danger::ServerCertVerifier for AcceptAnyServerCertificate {
20920        fn verify_server_cert(
20921            &self,
20922            _end_entity: &rustls::pki_types::CertificateDer<'_>,
20923            _intermediates: &[rustls::pki_types::CertificateDer<'_>],
20924            _server_name: &rustls::pki_types::ServerName<'_>,
20925            _ocsp_response: &[u8],
20926            _now: rustls::pki_types::UnixTime,
20927        ) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
20928            Ok(rustls::client::danger::ServerCertVerified::assertion())
20929        }
20930
20931        fn verify_tls12_signature(
20932            &self,
20933            _message: &[u8],
20934            _cert: &rustls::pki_types::CertificateDer<'_>,
20935            _dss: &rustls::DigitallySignedStruct,
20936        ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
20937            Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
20938        }
20939
20940        fn verify_tls13_signature(
20941            &self,
20942            _message: &[u8],
20943            _cert: &rustls::pki_types::CertificateDer<'_>,
20944            _dss: &rustls::DigitallySignedStruct,
20945        ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
20946            Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
20947        }
20948
20949        fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
20950            vec![
20951                rustls::SignatureScheme::RSA_PKCS1_SHA256,
20952                rustls::SignatureScheme::RSA_PKCS1_SHA384,
20953                rustls::SignatureScheme::RSA_PKCS1_SHA512,
20954                rustls::SignatureScheme::ECDSA_NISTP256_SHA256,
20955                rustls::SignatureScheme::ECDSA_NISTP384_SHA384,
20956                rustls::SignatureScheme::ECDSA_NISTP521_SHA512,
20957                rustls::SignatureScheme::ED25519,
20958                rustls::SignatureScheme::RSA_PSS_SHA256,
20959                rustls::SignatureScheme::RSA_PSS_SHA384,
20960                rustls::SignatureScheme::RSA_PSS_SHA512,
20961            ]
20962        }
20963    }
20964
20965    /// SNI the test certificate is registered under.
20966    const TEST_SNI: &str = "lolcatho.st";
20967
20968    /// Kernel send/receive queue size for the loopback pair, in bytes. Linux
20969    /// doubles and floors the request, so the effective queue is a few KiB —
20970    /// an order of magnitude under rustls's own 64 KiB record buffer, which is
20971    /// what makes `socket_write_vectored` return a partial count AND
20972    /// `WouldBlock` together instead of one or the other.
20973    const PINNED_SOCKET_QUEUE: usize = 4096;
20974
20975    /// Bytes the tests below queue as a response. Larger than rustls's record
20976    /// buffer, so one `socket_write_vectored` cannot absorb it all.
20977    const QUEUED_RESPONSE_LEN: usize = 256 * 1024;
20978
20979    /// Blocks the response is split into, so a gather offers several slices.
20980    const QUEUED_RESPONSE_CHUNK_LEN: usize = QUEUED_RESPONSE_LEN / 8;
20981
20982    /// Hard bound on the drive loops below. Reaching it is a test failure with
20983    /// the byte shortfall attached, never a silent pass.
20984    const MAX_DRIVE_TICKS: usize = 4096;
20985
20986    /// The response bytes, leaked once so blocks can be `Store::Static`.
20987    ///
20988    /// A repeating non-trivial pattern rather than a constant fill: a
20989    /// comparison against a constant fill cannot see bytes delivered out of
20990    /// order or a block delivered twice, which is half of what "no truncation"
20991    /// has to mean.
20992    ///
20993    /// Laid out as whole DATA frames on stream 1, each 16 KiB on the wire: a
20994    /// stream's `kawa.out` only ever holds whole H2 frames, and since #1604
20995    /// the write pass reads their headers to find where the frame a partial
20996    /// write cut ends (`h2_transmit::frame_tail`). The pattern runs through
20997    /// the headers as well, so the oracle still compares every byte.
20998    fn queued_response_body() -> &'static [u8] {
20999        const FRAME_LEN: usize = 16 * 1024;
21000        const PAYLOAD_LEN: usize = FRAME_LEN - 9;
21001        const _: () = assert!(QUEUED_RESPONSE_LEN.is_multiple_of(FRAME_LEN));
21002        static BODY: std::sync::OnceLock<&'static [u8]> = std::sync::OnceLock::new();
21003        BODY.get_or_init(|| {
21004            let mut body: Vec<u8> = (0..QUEUED_RESPONSE_LEN)
21005                .map(|index| (index % 251) as u8)
21006                .collect();
21007            for frame in body.chunks_mut(FRAME_LEN) {
21008                frame[..9].copy_from_slice(&[
21009                    (PAYLOAD_LEN >> 16) as u8,
21010                    (PAYLOAD_LEN >> 8) as u8,
21011                    PAYLOAD_LEN as u8,
21012                    0,
21013                    0,
21014                    0,
21015                    0,
21016                    0,
21017                    1,
21018                ]);
21019            }
21020            Box::leak(body.into_boxed_slice())
21021        })
21022    }
21023
21024    /// Move one TLS flight from `from` to `to`, in memory.
21025    fn pump_tls<A, B>(
21026        from: &mut rustls::ConnectionCommon<A>,
21027        to: &mut rustls::ConnectionCommon<B>,
21028    ) {
21029        let mut wire = Vec::new();
21030        while from.wants_write() {
21031            from.write_tls(&mut wire)
21032                .expect("a TLS flight must serialize into memory");
21033        }
21034        if wire.is_empty() {
21035            return;
21036        }
21037        let mut cursor = std::io::Cursor::new(wire.as_slice());
21038        while (cursor.position() as usize) < wire.len() {
21039            to.read_tls(&mut cursor)
21040                .expect("a TLS flight must be readable from memory");
21041            to.process_new_packets()
21042                .expect("a TLS flight must process cleanly");
21043        }
21044    }
21045
21046    /// A settled TLS 1.3 `FrontRustls` over a loopback socket whose kernel
21047    /// queues are pinned small, the peer end of that socket, and the client
21048    /// session that decrypts what the frontend writes.
21049    ///
21050    /// The peer and the client session are returned rather than kept here
21051    /// because dropping either would close the connection under the handler
21052    /// being tested, and because together they are the only oracle for "the
21053    /// bytes actually arrived".
21054    fn handshaken_front_rustls() -> (FrontRustls, std::net::TcpStream, rustls::ClientConnection) {
21055        let provider = std::sync::Arc::new(crate::crypto::default_provider());
21056
21057        let resolver = std::sync::Arc::new(crate::tls::MutexCertificateResolver::default());
21058        resolver
21059            .0
21060            .lock()
21061            .expect("the test resolver lock must be available")
21062            .add_certificate(&sozu_command::proto::command::AddCertificate {
21063                address: sozu_command::proto::command::SocketAddress::new_v4(127, 0, 0, 1, 8443),
21064                certificate: sozu_command::proto::command::CertificateAndKey {
21065                    certificate: include_str!("../../../assets/certificate.pem").to_owned(),
21066                    key: include_str!("../../../assets/key.pem").to_owned(),
21067                    names: vec![TEST_SNI.to_owned()],
21068                    ..Default::default()
21069                },
21070                expired_at: None,
21071            })
21072            .expect("the test certificate must load into the resolver");
21073
21074        let mut server_config = rustls::ServerConfig::builder_with_provider(provider.clone())
21075            .with_protocol_versions(&[&rustls::version::TLS13])
21076            .expect("the test provider must support TLS 1.3")
21077            .with_no_client_auth()
21078            .with_cert_resolver(resolver);
21079        // TLS 1.3 queues NewSessionTicket straight after the client Finished.
21080        // A server that sends tickets answers `socket_wants_write()` with
21081        // `true` before this fixture has queued a single response byte, and
21082        // every test below would then read `true` for the wrong reason.
21083        server_config.send_tls13_tickets = 0;
21084
21085        let client_config = rustls::ClientConfig::builder_with_provider(provider)
21086            .with_protocol_versions(&[&rustls::version::TLS13])
21087            .expect("the test provider must support TLS 1.3")
21088            .dangerous()
21089            .with_custom_certificate_verifier(std::sync::Arc::new(AcceptAnyServerCertificate))
21090            .with_no_client_auth();
21091
21092        let mut server = rustls::ServerConnection::new(std::sync::Arc::new(server_config))
21093            .expect("the test server session must initialize");
21094        let mut client = rustls::ClientConnection::new(
21095            std::sync::Arc::new(client_config),
21096            rustls::pki_types::ServerName::try_from(TEST_SNI)
21097                .expect("the test SNI must be a valid DNS name"),
21098        )
21099        .expect("the test client session must initialize");
21100
21101        for _ in 0..MAX_DRIVE_TICKS {
21102            if !client.is_handshaking()
21103                && !server.is_handshaking()
21104                && !client.wants_write()
21105                && !server.wants_write()
21106            {
21107                break;
21108            }
21109            pump_tls(&mut client, &mut server);
21110            pump_tls(&mut server, &mut client);
21111        }
21112
21113        assert!(
21114            !server.is_handshaking() && !client.is_handshaking(),
21115            "premise: the in-memory handshake must complete, or the session \
21116             under test buffers plaintext instead of encrypting it"
21117        );
21118        assert!(
21119            !server.wants_write(),
21120            "premise: a settled handshake must leave the server holding no \
21121             records, or socket_wants_write() answers true before this fixture \
21122             has queued one response byte"
21123        );
21124
21125        let listener =
21126            std::net::TcpListener::bind("127.0.0.1:0").expect("the test listener must bind");
21127        // Set on the listener so the accepted socket inherits it and the
21128        // advertised window starts small.
21129        socket2::SockRef::from(&listener)
21130            .set_recv_buffer_size(PINNED_SOCKET_QUEUE)
21131            .expect("the test listener receive queue must shrink");
21132        let address = listener
21133            .local_addr()
21134            .expect("the test listener must expose its address");
21135        let front = std::net::TcpStream::connect(address).expect("the loopback connect must land");
21136        let (peer, _peer_address) = listener.accept().expect("the test peer must be accepted");
21137        socket2::SockRef::from(&front)
21138            .set_send_buffer_size(PINNED_SOCKET_QUEUE)
21139            .expect("the frontend send queue must shrink");
21140        socket2::SockRef::from(&peer)
21141            .set_recv_buffer_size(PINNED_SOCKET_QUEUE)
21142            .expect("the peer receive queue must shrink");
21143        front
21144            .set_nonblocking(true)
21145            .expect("mio requires a non-blocking stream");
21146        peer.set_nonblocking(true)
21147            .expect("the peer must not block the drive loop");
21148
21149        let socket = FrontRustls {
21150            stream: mio::net::TcpStream::from_std(front),
21151            session: server,
21152            peer_disconnected: false,
21153            peer_reset: false,
21154            tls_fatal: false,
21155            session_ulid: Ulid::generate(),
21156            configured_peer: None,
21157            recv_memory: Default::default(),
21158        };
21159        (socket, peer, client)
21160    }
21161
21162    /// A server `ConnectionH2` over that handler.
21163    ///
21164    /// `state` is a parameter for the same reason
21165    /// `connection_with_backpressure` takes one: the close decision reads it
21166    /// as `H2State::GoAway` and the write pass reads it as `H2State::Header`.
21167    pub(in crate::protocol::mux) fn rustls_h2_connection(
21168        pool: &Rc<RefCell<Pool>>,
21169        state: H2State,
21170    ) -> (
21171        H2Shell<FrontRustls>,
21172        std::net::TcpStream,
21173        rustls::ClientConnection,
21174    ) {
21175        let (socket, peer, client) = handshaken_front_rustls();
21176        let mut connection = H2Shell::new(
21177            Ulid::generate(),
21178            socket,
21179            Position::Server,
21180            &mut PoolBufferSource::new(Rc::downgrade(pool)),
21181            H2FloodConfig::default(),
21182            H2ConnectionConfig::default(),
21183            Duration::from_secs(30),
21184            None,
21185            Duration::from_secs(30),
21186            Some((H2StreamId::Zero, CLIENT_PREFACE_SIZE)),
21187            Ready::WRITABLE | Ready::HUP | Ready::ERROR,
21188        )
21189        .expect("a pool with free buffers must yield an H2 connection");
21190        connection.core.state = state;
21191        (connection, peer, client)
21192    }
21193
21194    /// Read every TLS record the peer's kernel currently holds, decrypt it,
21195    /// and append the plaintext to `received`.
21196    fn drain_peer(
21197        client: &mut rustls::ClientConnection,
21198        peer: &mut std::net::TcpStream,
21199        received: &mut Vec<u8>,
21200    ) {
21201        loop {
21202            match client.read_tls(peer) {
21203                Ok(0) => break,
21204                Ok(_) => {}
21205                Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => break,
21206                Err(error) => {
21207                    panic!("the peer must read the frontend's TLS records: {error:?}")
21208                }
21209            }
21210            client
21211                .process_new_packets()
21212                .expect("the peer must decrypt what the frontend wrote");
21213            take_plaintext(client, received);
21214        }
21215        take_plaintext(client, received);
21216    }
21217
21218    /// Drain the client session's decrypted plaintext buffer.
21219    ///
21220    /// `WouldBlock` here means "nothing decrypted yet", not an error, and
21221    /// `read_to_end` appends whatever it did read before reporting it.
21222    fn take_plaintext(client: &mut rustls::ClientConnection, received: &mut Vec<u8>) {
21223        use std::io::Read as _;
21224        let mut plaintext = Vec::new();
21225        match client.reader().read_to_end(&mut plaintext) {
21226            Ok(_) => {}
21227            Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {}
21228            Err(error) => panic!("the peer must decrypt the frontend's records: {error:?}"),
21229        }
21230        received.extend_from_slice(&plaintext);
21231    }
21232
21233    /// Keep flushing the handler's buffered records while the peer reads,
21234    /// until rustls holds nothing. Returns whether it got there.
21235    fn flush_until_drained(
21236        socket: &mut FrontRustls,
21237        client: &mut rustls::ClientConnection,
21238        peer: &mut std::net::TcpStream,
21239        received: &mut Vec<u8>,
21240    ) -> bool {
21241        for _ in 0..MAX_DRIVE_TICKS {
21242            if !socket.socket_wants_write() {
21243                return true;
21244            }
21245            // Loopback delivery is softirq-driven, not synchronous with
21246            // `write_tls`: give it a slot rather than spinning the CPU.
21247            std::thread::yield_now();
21248            drain_peer(client, peer, received);
21249            socket.socket_write(&[]);
21250        }
21251        !socket.socket_wants_write()
21252    }
21253
21254    /// A TLS frontend whose final GOAWAY went out without `close_notify`
21255    /// behind it: the close its next writable pass decides queues the alert
21256    /// first and waits a pass for it, then starts the lingering close. The
21257    /// first pass's close moves the state to `Error`; the pending linger is
21258    /// what keeps the second pass from closing outright. The client reads
21259    /// the alert, then the EOF of the half-close.
21260    ///
21261    /// TO SEE THIS RED: in `H2Shell::linger_instead_of_closing`, drop the
21262    /// `self.core.linger = Linger::Pending { deadline };` before the deferral.
21263    /// The second pass then returns `CloseSession`.
21264    #[test]
21265    fn a_linger_deferred_behind_close_notify_starts_on_the_next_pass() {
21266        use std::io::Read as _;
21267
21268        let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
21269        let (mut connection, mut peer, mut client) = rustls_h2_connection(&pool, H2State::GoAway);
21270        connection.core.drain.__test_set_draining();
21271        connection.core.graceful_goaway = true;
21272        let mut context = test_context(&pool);
21273        let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
21274
21275        let first = connection.writable(&mut context, EndpointClient(&mut router));
21276        assert!(
21277            matches!(first, MuxResult::Continue) && !connection.core.is_lingering(),
21278            "the first pass waits for close_notify, got {first:?}"
21279        );
21280        let mut second = MuxResult::Continue;
21281        for _ in 0..MAX_DRIVE_TICKS {
21282            connection.core.readiness.event.insert(Ready::WRITABLE);
21283            second = connection.writable(&mut context, EndpointClient(&mut router));
21284            if connection.core.is_lingering() || !matches!(second, MuxResult::Continue) {
21285                break;
21286            }
21287            std::thread::yield_now();
21288        }
21289        assert!(
21290            matches!(second, MuxResult::Continue) && connection.core.is_lingering(),
21291            "once close_notify is out the connection lingers, got {second:?}"
21292        );
21293
21294        let mut received = Vec::new();
21295        let mut closed = false;
21296        for _ in 0..MAX_DRIVE_TICKS {
21297            drain_peer(&mut client, &mut peer, &mut received);
21298            let mut plaintext = [0u8; 16];
21299            match client.reader().read(&mut plaintext) {
21300                Ok(0) => {
21301                    closed = true;
21302                    break;
21303                }
21304                Ok(_) => {}
21305                Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
21306                    std::thread::yield_now()
21307                }
21308                Err(error) => panic!("the client must see a clean close: {error:?}"),
21309            }
21310        }
21311        assert!(
21312            closed,
21313            "the client reads close_notify, then the end of the stream"
21314        );
21315    }
21316
21317    /// The final GOAWAY and the TLS `close_notify` leave in ONE write, and the
21318    /// peer reads the GOAWAY before the close.
21319    ///
21320    /// Drives what a client GOAWAY on an idle connection sets off:
21321    /// `goaway(NoError)`, the `writable` pass that flushes it and starts the
21322    /// lingering close, and `close`. Every TLS write in between is counted through
21323    /// `crate::socket::tls_writes`, which counts each `write_tls` holding at
21324    /// least one record, i.e. each `writev(2)` `FrontRustls` issues. The wire
21325    /// is then read raw, so the ORDER is checked on the records themselves
21326    /// (TLS 1.3 hides the inner type, not the length: 5 + 17 + 1 + 16 = 39 for
21327    /// the GOAWAY, 5 + 2 + 1 + 16 = 24 for the alert), and decrypted, so the
21328    /// client sees the 17-byte GOAWAY and then a clean end of stream, which
21329    /// rustls reports only once it has read `close_notify`.
21330    ///
21331    /// TO SEE THIS RED: in `H2Shell::flush_output_to_socket`, drop the
21332    /// `output_flush_closes_connection` branch so every output flush goes
21333    /// through `socket_write`. The GOAWAY then leaves alone and `close` sends the
21334    /// alert in a second write: this test fails on `the final GOAWAY and
21335    /// close_notify must share one TLS write`, with 2 writes. For the order:
21336    /// in `FrontRustls::socket_write_then_close`, call `send_close_notify`
21337    /// before `writer().write`; it fails on the records, `[24, 39]`.
21338    #[test]
21339    fn the_final_goaway_and_close_notify_leave_in_one_tls_write() {
21340        use std::io::Read as _;
21341
21342        let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
21343        let (mut connection, mut peer, mut client) = rustls_h2_connection(&pool, H2State::Header);
21344        let mut context = test_context(&pool);
21345        let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
21346
21347        let writes_before = crate::socket::tls_writes();
21348        let queued = connection.core.goaway(H2Error::NoError);
21349        assert!(
21350            matches!(queued, MuxResult::Continue),
21351            "premise: the GOAWAY must serialise, got {queued:?}"
21352        );
21353        let result = connection.writable(&mut context, EndpointClient(&mut router));
21354        // The close that pass decides is a lingering one: the write side is
21355        // shut down behind the GOAWAY and `close_notify`, and what the client
21356        // still sends is drained (`H2Shell::linger_instead_of_closing`).
21357        assert!(
21358            matches!(result, MuxResult::Continue) && connection.core.is_lingering(),
21359            "the pass that flushed the final GOAWAY must start the lingering close, got {result:?}"
21360        );
21361        connection.close(&mut context, EndpointClient(&mut router));
21362        assert_eq!(
21363            crate::socket::tls_writes() - writes_before,
21364            1,
21365            "the final GOAWAY and close_notify must share one TLS write"
21366        );
21367
21368        let mut wire = Vec::new();
21369        for _ in 0..MAX_DRIVE_TICKS {
21370            let mut chunk = [0u8; 256];
21371            match peer.read(&mut chunk) {
21372                Ok(0) => break,
21373                Ok(size) => wire.extend_from_slice(&chunk[..size]),
21374                Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
21375                    if wire.len() >= 39 + 24 {
21376                        break;
21377                    }
21378                    std::thread::yield_now();
21379                }
21380                Err(error) => panic!("the peer must read the frontend's records: {error:?}"),
21381            }
21382        }
21383        let mut records = Vec::new();
21384        let mut offset = 0;
21385        while offset + 5 <= wire.len() {
21386            let length = 5 + usize::from(u16::from_be_bytes([wire[offset + 3], wire[offset + 4]]));
21387            records.push(length);
21388            offset += length;
21389        }
21390        assert_eq!(
21391            records,
21392            vec![39, 24],
21393            "the peer must receive the GOAWAY record, then the close_notify record"
21394        );
21395
21396        let mut cursor = std::io::Cursor::new(wire.as_slice());
21397        while (cursor.position() as usize) < wire.len() {
21398            client
21399                .read_tls(&mut cursor)
21400                .expect("the peer must read the frontend's records");
21401            client
21402                .process_new_packets()
21403                .expect("the peer must decrypt the frontend's records");
21404        }
21405        let mut frame = [0u8; 64];
21406        let size = client
21407            .reader()
21408            .read(&mut frame)
21409            .expect("the GOAWAY must be readable before the close");
21410        assert_eq!(size, 17, "the GOAWAY frame is 9 + 8 bytes");
21411        assert_eq!(frame[3], 0x07, "the first plaintext must be a GOAWAY frame");
21412        assert_eq!(
21413            &frame[13..17],
21414            &[0, 0, 0, 0],
21415            "the GOAWAY must carry NO_ERROR"
21416        );
21417        assert_eq!(
21418            client
21419                .reader()
21420                .read(&mut frame)
21421                .expect("a close_notify ends the stream cleanly"),
21422            0,
21423            "the close_notify must follow the GOAWAY"
21424        );
21425    }
21426
21427    /// `FrontRustls` answers a blocked write with bytes taken AND `WouldBlock`,
21428    /// and `update_readiness` calls that pair not-stalled.
21429    ///
21430    /// This is the premise every scripted `(size > 0, WouldBlock)` test in this
21431    /// file rests on, taken here from the production handler instead of from a
21432    /// fixture: rustls absorbs plaintext into its record buffer while
21433    /// `write_tls` blocks against the kernel, so `buffered_size` and
21434    /// `can_write` disagree and one return value carries both answers.
21435    ///
21436    /// TO SEE THIS RED: in `FrontRustls::socket_write_vectored`, report
21437    /// nothing absorbed on a blocked write — change the `!can_write` arm of
21438    /// the final result from `(buffered_size, SocketResult::WouldBlock)` to
21439    /// `(0, SocketResult::WouldBlock)`. That is the under-reporting half of
21440    /// the same class the over-reporting one below belongs to, and the caller
21441    /// then re-offers bytes rustls already holds. This test fails on `rustls
21442    /// takes plaintext off the caller's hands even while the kernel is full,
21443    /// so the handler must report the bytes it absorbed`. Measured: `1092
21444    /// passed; 6 failed` — every test in this block and nothing else, so the
21445    /// whole class was invisible to the suite.
21446    ///
21447    /// Not a red recipe, and measured as such: deleting `can_write = false;`
21448    /// from the `ErrorKind::WouldBlock` arm of the partial-absorb draining
21449    /// loop leaves the entire suite green. The post-loop flush block sets the
21450    /// same flag for the same reason a moment later, so the `WouldBlock` half
21451    /// of this pair is guarded twice in production and no single-site deletion
21452    /// of it is observable here.
21453    #[test]
21454    fn a_real_rustls_frontend_reports_bytes_taken_and_would_block_together() {
21455        let (mut socket, _peer, _client) = handshaken_front_rustls();
21456        let body = queued_response_body();
21457
21458        assert!(
21459            !socket.socket_wants_write(),
21460            "premise: the fixture must start with rustls holding nothing"
21461        );
21462
21463        let (size, status) = socket.socket_write_vectored(&[IoSlice::new(body)]);
21464
21465        assert!(
21466            size > 0,
21467            "rustls takes plaintext off the caller's hands even while the \
21468             kernel is full, so the handler must report the bytes it absorbed"
21469        );
21470        assert!(
21471            size < body.len(),
21472            "premise: the offer must exceed what one call can absorb, or the \
21473             partial-write path that produces this pair is never entered \
21474             (absorbed {size} of {})",
21475            body.len()
21476        );
21477        assert_eq!(
21478            status,
21479            SocketResult::WouldBlock,
21480            "a frontend whose kernel send queue is full must report WouldBlock"
21481        );
21482        assert!(
21483            socket.socket_wants_write(),
21484            "the records rustls could not push must still be waiting, which is \
21485             the only reason any close path has to ask"
21486        );
21487
21488        let mut readiness = Readiness {
21489            event: Ready::WRITABLE,
21490            interest: Ready::WRITABLE | Ready::HUP | Ready::ERROR,
21491        };
21492        assert!(
21493            !update_readiness_after_write(size, status, &mut readiness),
21494            "a write that moved bytes is not a stall however it reported its \
21495             status: a pass that ends here leaves the rest of the response \
21496             queued behind a WouldBlock the rule does not treat as one"
21497        );
21498    }
21499
21500    /// The empty flush the close paths issue really does hand rustls's records
21501    /// to the kernel — `true` before it and `false` after, once the peer reads.
21502    ///
21503    /// `socket_write(&[])` is the middle call of every flush triple in
21504    /// `h2.rs`: those sites discard its `(size, status)` entirely and learn
21505    /// whether it landed only by asking `socket_wants_write()` again. That
21506    /// reconstruction is only sound if a zero-length write flushes at all, and
21507    /// `FrontRustls::socket_write`'s main loop exits immediately for an empty
21508    /// buffer — the flush lives in a block after it, reached by no other path.
21509    ///
21510    /// TO SEE THIS RED: in `FrontRustls::socket_write`, delete the post-loop
21511    /// `if !is_error && !is_closed && can_write && self.session.wants_write()`
21512    /// block. `socket_write(&[])` becomes a no-op, the records never reach the
21513    /// kernel, and this test fails on `once the peer reads, the empty flush
21514    /// must hand every record to the kernel: rustls still holds records after
21515    /// 4096 flushes against a draining peer`. Measured: `1093 passed; 5
21516    /// failed` — five of the six here and nothing that predates them, so the
21517    /// block that comment calls load-bearing had no unit coverage at all.
21518    #[test]
21519    fn an_empty_flush_drains_the_records_a_blocked_write_left_behind() {
21520        let (mut socket, mut peer, mut client) = handshaken_front_rustls();
21521        let body = queued_response_body();
21522
21523        let (absorbed, _status) = socket.socket_write_vectored(&[IoSlice::new(body)]);
21524        assert!(
21525            socket.socket_wants_write(),
21526            "premise: a blocked kernel must leave records pending, or neither \
21527             arm below is reachable"
21528        );
21529
21530        // The kernel is still full: the flush cannot land and the query
21531        // answers `true` on both sides of it. This is the "true twice" arm.
21532        socket.socket_write(&[]);
21533        assert!(
21534            socket.socket_wants_write(),
21535            "a flush against a full kernel leaves the records where they were, \
21536             so the triple's second query must still answer true"
21537        );
21538
21539        // Now the peer reads, and the same flush must land. This is the
21540        // "true then false" arm.
21541        let mut received = Vec::new();
21542        assert!(
21543            flush_until_drained(&mut socket, &mut client, &mut peer, &mut received),
21544            "once the peer reads, the empty flush must hand every record to \
21545             the kernel: rustls still holds records after {MAX_DRIVE_TICKS} \
21546             flushes against a draining peer"
21547        );
21548
21549        for _ in 0..MAX_DRIVE_TICKS {
21550            if received.len() >= absorbed {
21551                break;
21552            }
21553            std::thread::yield_now();
21554            drain_peer(&mut client, &mut peer, &mut received);
21555        }
21556        assert_eq!(
21557            received.len(),
21558            absorbed,
21559            "every plaintext byte rustls reported absorbing must reach the peer"
21560        );
21561        assert_eq!(
21562            received.as_slice(),
21563            &body[..absorbed],
21564            "the peer must receive those bytes unchanged and in order"
21565        );
21566    }
21567
21568    /// A response larger than one pass can deliver reaches the peer in full,
21569    /// driven through `writable()` over the production TLS handler.
21570    ///
21571    /// This is #1454's truncation vector end to end, with no scripted answer
21572    /// anywhere: the `(size > 0, WouldBlock)` and `(0, WouldBlock)` pairs come
21573    /// from `FrontRustls` against a real full kernel, the stall and its resume
21574    /// come from `write_streams`, and the oracle is the bytes a real
21575    /// `rustls::ClientConnection` decrypts on the other end of the socket. The
21576    /// assertion is byte equality, not a call count, so it says nothing about
21577    /// how the write machine is shaped and everything about what it delivers.
21578    ///
21579    /// TO SEE THIS RED: in `FrontRustls::socket_write_vectored`, report the
21580    /// full offer instead of what rustls took — replace the final
21581    /// `(buffered_size, SocketResult::WouldBlock)` with
21582    /// `(total_len, SocketResult::WouldBlock)`. That is the over-reporting
21583    /// shape `socket_write`/`socket_write_vectored`'s own comments call the
21584    /// 4.5 MB truncation-class bug: `h2_transmit::confirm` then consumes bytes
21585    /// the socket never encrypted and they are gone. Measured: `1095 passed; 3
21586    /// failed`, this one on `the response was truncated at 65536 of 262144
21587    /// bytes after 4096 passes, with 0 blocks still queued`. The pass empties
21588    /// `kawa.out` against bytes that were never sent, `finalize_write` reads
21589    /// that as everything written and quiesces, and the remaining 196608 bytes
21590    /// have no route to the wire at all — the connection is not short, it is
21591    /// hung, and the tick cap is what converts that into a failure instead of
21592    /// a wait. Nothing that predates this block moves.
21593    #[test]
21594    fn a_blocked_rustls_frontend_delivers_every_queued_byte_across_passes() {
21595        let pool = make_pool_for_invariant_16();
21596        let (mut connection, mut peer, mut client) = rustls_h2_connection(&pool, H2State::Header);
21597        let mut context = test_context(&pool);
21598        let body = queued_response_body();
21599
21600        let gid = context
21601            .create_stream(Ulid::generate(), 1 << 16)
21602            .expect("test context must create a stream");
21603        connection
21604            .core
21605            .stream_table
21606            .register(REGISTERED_STREAM_ID, gid, connection.core.now);
21607        for chunk in body.chunks(QUEUED_RESPONSE_CHUNK_LEN) {
21608            context.streams[gid]
21609                .back
21610                .out
21611                .push_back(kawa::OutBlock::Store(kawa::Store::Static(chunk)));
21612        }
21613        assert_prepare_gate_is_shut(&context, gid);
21614        assert_eq!(
21615            connection.core.stream_table.expect_write(),
21616            None,
21617            "premise: no parked write, so the first pass takes the main loop"
21618        );
21619
21620        let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
21621        let mut received = Vec::new();
21622        let mut ticks = 0usize;
21623
21624        loop {
21625            // What the event loop does: report the socket writable, run the
21626            // pass, and let the peer read whatever reached the kernel.
21627            connection.core.readiness.event.insert(Ready::WRITABLE);
21628            connection.writable(&mut context, EndpointClient(&mut router));
21629            std::thread::yield_now();
21630            drain_peer(&mut client, &mut peer, &mut received);
21631
21632            if received.len() >= body.len() {
21633                break;
21634            }
21635            ticks += 1;
21636            assert!(
21637                ticks < MAX_DRIVE_TICKS,
21638                "the response was truncated at {} of {} bytes after {ticks} \
21639                 passes, with {} blocks still queued",
21640                received.len(),
21641                body.len(),
21642                context.streams[gid].back.out.len()
21643            );
21644            // Owed, not undelivered: once `kawa.out` is empty and rustls
21645            // holds nothing, every byte has been handed to the kernel and
21646            // `finalize_write` may legitimately quiesce while the last of it
21647            // is still crossing loopback. Asserting on `received` here would
21648            // blame the write machine for the harness's own race.
21649            let still_owed =
21650                !context.streams[gid].back.out.is_empty() || connection.socket.socket_wants_write();
21651            assert!(
21652                !still_owed || connection.core.readiness.interest.is_writable(),
21653                "a pass that still owes bytes must keep WRITABLE interest, or \
21654                 no later event reaches this connection and the response is \
21655                 truncated at {} of {} bytes with {} blocks queued",
21656                received.len(),
21657                body.len(),
21658                context.streams[gid].back.out.len()
21659            );
21660        }
21661
21662        assert_eq!(
21663            received.len(),
21664            body.len(),
21665            "the peer must receive every queued byte"
21666        );
21667        assert_eq!(
21668            received.as_slice(),
21669            body,
21670            "the peer must receive the queued bytes unchanged and in order"
21671        );
21672        assert!(
21673            context.streams[gid].back.out.is_empty(),
21674            "every queued block must be consumed once the response is delivered"
21675        );
21676        assert_eq!(
21677            context.streams[gid].metrics.bout,
21678            body.len(),
21679            "the stream must account exactly the bytes the socket took"
21680        );
21681    }
21682
21683    /// `force_disconnect`'s server arm, over the handler whose
21684    /// `socket_wants_write()` is the only production implementation that can
21685    /// answer `true`: it re-arms while rustls holds records and closes once
21686    /// they are gone.
21687    ///
21688    /// One test for both arms because the TRANSITION is the claim — the same
21689    /// connection, the same records, and the only thing that changes is
21690    /// whether the peer read them. A pair of connections would prove the two
21691    /// answers of a table `h2_close::force_disconnect_action` already tables
21692    /// exhaustively; what has never been proved is that this caller reads a
21693    /// real handler's answer at all.
21694    ///
21695    /// TO SEE THIS RED: in `ConnectionH2::force_disconnect`, replace
21696    /// `let tls_wants_write = self.tls_wants_write();` with
21697    /// `let tls_wants_write = false;` — the decision stops reading the socket,
21698    /// and the call closes the session while records are pending, which sends
21699    /// FIN and destroys them. This test fails on `a disconnect with records
21700    /// pending must not close the session: closing sends FIN and destroys the
21701    /// records rustls still holds, which the peer reads as a truncated
21702    /// response`. Measured: `1096 passed; 2 failed` — this test and
21703    /// `force_disconnect_re_arms_while_tls_records_are_pending`, the same
21704    /// decision with one witness over a modelled handler and one over the
21705    /// production one.
21706    #[test]
21707    fn force_disconnect_over_a_real_rustls_frontend_waits_for_the_records_to_drain() {
21708        let pool = make_pool_for_invariant_16();
21709        let (mut connection, mut peer, mut client) = rustls_h2_connection(&pool, H2State::GoAway);
21710        let body = queued_response_body();
21711
21712        let (absorbed, _status) = connection
21713            .socket
21714            .socket_write_vectored(&[IoSlice::new(body)]);
21715        assert!(
21716            absorbed > 0 && connection.socket.socket_wants_write(),
21717            "premise: the blocked kernel must leave real records pending"
21718        );
21719        assert!(
21720            !connection.core.peer_gone_after_final_goaway(),
21721            "premise: the peer must still be live, or the decision \
21722             short-circuits on its first boolean and never reads the socket"
21723        );
21724
21725        assert!(
21726            matches!(connection.force_disconnect(), MuxResult::Continue),
21727            "a disconnect with records pending must not close the session: \
21728             closing sends FIN and destroys the records rustls still holds, \
21729             which the peer reads as a truncated response"
21730        );
21731        assert!(
21732            connection.core.readiness.interest.is_writable(),
21733            "the delayed close must keep WRITABLE interest so the write path \
21734             can still flush"
21735        );
21736        // The records were written straight to the socket, outside any
21737        // writable pass, so no refusal is recorded and the delayed close
21738        // re-raises WRITABLE for the write path to run once more. A write the
21739        // same pass saw refused would leave it to the kernel's edge instead
21740        // (`a_refused_goaway_flush_waits_for_the_kernel_edge_then_closes`).
21741        assert!(
21742            connection.core.readiness.event.is_writable(),
21743            "with no refused write recorded, ensure_tls_flushed must re-signal \
21744             the WRITABLE event so the write path runs again"
21745        );
21746
21747        let mut received = Vec::new();
21748        assert!(
21749            flush_until_drained(
21750                &mut connection.socket,
21751                &mut client,
21752                &mut peer,
21753                &mut received
21754            ),
21755            "premise for the second arm: the records must actually drain once \
21756             the peer reads"
21757        );
21758
21759        assert!(
21760            matches!(connection.force_disconnect(), MuxResult::CloseSession),
21761            "with rustls holding nothing, the same call must close: delaying \
21762             further would strand a connection with no reason left to wait"
21763        );
21764    }
21765
21766    /// The `(H2State::Error, Position::Server)` arm of
21767    /// `ConnectionH2::dispatch_writable_state` reads the real handler's
21768    /// POST-flush answer: it keeps the session open while rustls still holds
21769    /// records — leaving WRITABLE to the kernel's edge, since the flush was
21770    /// refused — and closes once they are gone. The test drains the records
21771    /// itself between the two passes (`flush_until_drained`); the second pass
21772    /// only has to see that nothing is left.
21773    ///
21774    /// That arm has no flush of its own — `H2Shell::writable`'s preamble
21775    /// already issued this pass's `socket_write(&[])` and discarded both the
21776    /// size and the status it returned, and the `tls_wants_write` the arm is
21777    /// handed is the read after it, so that query is the only thing that can
21778    /// tell this site whether the flush landed. Until now the arm was
21779    /// exercised only as `h2_close::error_close_action`'s pure table; no test
21780    /// reached it through a connection whose handler could answer `true`,
21781    /// because none existed.
21782    ///
21783    /// TO SEE THIS RED: in that arm, replace
21784    /// `h2_close::error_close_action(tls_wants_write)` with
21785    /// `h2_close::error_close_action(false)` — the decision stops reading the
21786    /// socket and closes with records pending, sending FIN over plaintext the
21787    /// peer never received. Measured, this test fails on `a pass that finds
21788    /// records still pending after its own flush must not close the session:
21789    /// the close destroys plaintext the peer has not received and it reads the
21790    /// response as truncated`. Measured: `1116 passed; 1 failed` — it is the
21791    /// only test in the crate that moves.
21792    #[test]
21793    fn a_rustls_frontend_in_error_state_stays_open_until_its_records_drain() {
21794        let pool = make_pool_for_invariant_16();
21795        let (mut connection, mut peer, mut client) = rustls_h2_connection(&pool, H2State::Error);
21796        let mut context = test_context(&pool);
21797        let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
21798        let body = queued_response_body();
21799
21800        let (absorbed, _status) = connection
21801            .socket
21802            .socket_write_vectored(&[IoSlice::new(body)]);
21803        assert!(
21804            absorbed > 0 && connection.socket.socket_wants_write(),
21805            "premise: the blocked kernel must leave real records pending, or \
21806             this arm reads its only input as false and the test proves nothing"
21807        );
21808
21809        connection.core.readiness.event.insert(Ready::WRITABLE);
21810        assert!(
21811            matches!(
21812                connection.writable(&mut context, EndpointClient(&mut router)),
21813                MuxResult::Continue
21814            ),
21815            "a pass that finds records still pending after its own flush must \
21816             not close the session: the close destroys plaintext the peer has \
21817             not received and it reads the response as truncated"
21818        );
21819        // The kernel refused the flush: it owes this socket its next
21820        // WRITABLE edge, so no synthetic one is queued (sozu-proxy/sozu#1788).
21821        assert!(
21822            connection.core.readiness.interest.is_writable(),
21823            "the delayed close must keep WRITABLE interest so the kernel edge \
21824             is delivered"
21825        );
21826        assert!(
21827            !connection.core.readiness.event.is_writable(),
21828            "a flush the kernel refused must leave WRITABLE to the kernel's \
21829             next edge, got {:?}",
21830            connection.core.readiness
21831        );
21832
21833        let mut received = Vec::new();
21834        assert!(
21835            flush_until_drained(
21836                &mut connection.socket,
21837                &mut client,
21838                &mut peer,
21839                &mut received
21840            ),
21841            "premise for the second arm: the records must actually drain once \
21842             the peer reads"
21843        );
21844
21845        connection.core.readiness.event.insert(Ready::WRITABLE);
21846        assert!(
21847            matches!(
21848                connection.writable(&mut context, EndpointClient(&mut router)),
21849                MuxResult::CloseSession
21850            ),
21851            "with rustls holding nothing, the same pass must close: the bytes \
21852             are all in the kernel and waiting longer strands the session"
21853        );
21854    }
21855
21856    /// The `H2State::GoAway` arm of `ConnectionH2::dispatch_writable_state`
21857    /// and the `ConnectionH2::dispatch_writable_state_after_flush` that
21858    /// settles it — the triple whose own comment calls it the primary
21859    /// truncation vector under HAProxy chaining — over the production TLS
21860    /// handler: it keeps the session open while rustls still holds records,
21861    /// leaving WRITABLE to the kernel's edge, and disconnects once they are
21862    /// gone. As in the Error-arm test, the records are drained by the test
21863    /// between the passes, not by the second pass.
21864    ///
21865    /// This is the triple #1454 names. Of the three, `finalize_write`'s is
21866    /// driven by `a_blocked_rustls_frontend_delivers_every_queued_byte_across_passes`
21867    /// above and the Error arm by
21868    /// `a_rustls_frontend_in_error_state_stays_open_until_its_records_drain`; this
21869    /// one completes the set. The fourth `socket_write(&[])` site,
21870    /// `flush_output_buffer`, is deliberately not targeted here: it keeps the
21871    /// status its flush returned instead of re-querying, so it is not this
21872    /// shape and a test aimed at it would prove nothing about the vector.
21873    ///
21874    /// The state assertion is not decoration. Falling through this arm reaches
21875    /// `force_disconnect`, which has a record guard of its own and answers
21876    /// `MuxResult::Continue` too — so the result alone cannot tell a correct
21877    /// hold from a fall-through that only looks correct. `H2State::Error` is
21878    /// what the fall-through leaves behind, and nothing else sets it here.
21879    ///
21880    /// TO SEE THIS RED: in `ConnectionH2::dispatch_writable_state_after_flush`,
21881    /// replace the `h2_close::goaway_close_action(TlsFlushPhase::AfterFlush,
21882    /// false, tls_wants_write)` third argument with `false`. The arm
21883    /// stops reading whether the flush `H2Shell::writable` performed for
21884    /// it landed, falls through to
21885    /// `force_disconnect`, and this test fails on `a GoAway pass whose flush
21886    /// did not land must stay in GoAway`. Measured: `1115 passed; 2 failed` —
21887    /// this test and `a_flush_that_does_not_drain_keeps_the_connection_open`,
21888    /// the same arm with one witness over a modelled handler and one over the
21889    /// production one.
21890    #[test]
21891    fn a_rustls_frontend_in_goaway_stays_open_until_its_records_drain() {
21892        let pool = make_pool_for_invariant_16();
21893        let (mut connection, mut peer, mut client) = rustls_h2_connection(&pool, H2State::GoAway);
21894        let mut context = test_context(&pool);
21895        let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
21896        let body = queued_response_body();
21897
21898        let (absorbed, _status) = connection
21899            .socket
21900            .socket_write_vectored(&[IoSlice::new(body)]);
21901        assert!(
21902            absorbed > 0 && connection.socket.socket_wants_write(),
21903            "premise: the blocked kernel must leave real records pending"
21904        );
21905        assert!(
21906            !connection.core.peer_gone_after_final_goaway(),
21907            "premise: the peer must still be live, or the decision \
21908             short-circuits on its first boolean and never reads the socket"
21909        );
21910
21911        connection.core.readiness.event.insert(Ready::WRITABLE);
21912        assert!(
21913            matches!(
21914                connection.writable(&mut context, EndpointClient(&mut router)),
21915                MuxResult::Continue
21916            ),
21917            "a GoAway pass that finds records still pending after its own \
21918             flush must not close: FIN here destroys plaintext the peer never \
21919             received"
21920        );
21921        assert!(
21922            matches!(connection.core.state, H2State::GoAway),
21923            "a GoAway pass whose flush did not land must stay in GoAway: \
21924             reaching Error means the arm fell through to force_disconnect \
21925             and its own record guard, not this one, kept the session alive"
21926        );
21927        // The kernel refused the flush: it owes this socket its next
21928        // WRITABLE edge, so no synthetic one is queued (sozu-proxy/sozu#1788).
21929        assert!(
21930            !connection.core.readiness.event.is_writable(),
21931            "a flush the kernel refused must leave WRITABLE to the kernel's \
21932             next edge, got {:?}",
21933            connection.core.readiness
21934        );
21935
21936        let mut received = Vec::new();
21937        assert!(
21938            flush_until_drained(
21939                &mut connection.socket,
21940                &mut client,
21941                &mut peer,
21942                &mut received
21943            ),
21944            "premise for the second arm: the records must actually drain once \
21945             the peer reads"
21946        );
21947
21948        connection.core.readiness.event.insert(Ready::WRITABLE);
21949        assert!(
21950            matches!(
21951                connection.writable(&mut context, EndpointClient(&mut router)),
21952                MuxResult::CloseSession
21953            ),
21954            "with rustls holding nothing, the same pass must disconnect: every \
21955             byte is in the kernel and waiting longer strands the session"
21956        );
21957    }
21958
21959    // ── #1498: a DRIVEN TLS close_notify over a real `FrontRustls` ─────
21960    //
21961    // `H2Shell::begin_tls_close` is the seam that reaches
21962    // `FrontRustls::socket_close`, which is
21963    // `rustls::ServerConnection::send_close_notify`. Replacing its body with
21964    // `{}` at 0dddedf2 left the WHOLE suite green — `sozu-lib` `1114 passed;
21965    // 0 failed`, and no `sozu-e2e` test reddened on the mutation either —
21966    // while the same mutation on either of its two siblings from the same
21967    // extraction reddens at once. Measured against this branch's own `1115
21968    // passed` baseline: `tls_wants_write() -> false` gives `1104 passed; 11
21969    // failed` and a no-op `flush_tls_records()` gives `1110 passed; 5
21970    // failed`, ten and four of those respectively being tests OTHER than this
21971    // one — which is the asymmetry the whole file turns on. #1484's
21972    // coverage audit reached the same conclusion from the other direction,
21973    // listing `initiate_close_notify` among the `socket_wants_write` branches
21974    // its six `H2Shell<FrontRustls>` tests could not reach, for want of a
21975    // driven close_notify.
21976    //
21977    // The hole is structural rather than an oversight. Every handler the suite
21978    // drives a close over — `mio::net::TcpStream` and
21979    // `BackpressuredTlsSocket` — inherits `SocketHandler::socket_close`'s EMPTY
21980    // trait default, so to all of them the real seam and a no-op are the same
21981    // thing. `FrontRustls` is the only implementation in the tree that
21982    // overrides it, which is why the test below has to build one.
21983    //
21984    // And the assertion has to be on the ALERT, not on a FIN. That distinction
21985    // is the whole point of the alert: rustls answers `Ok(0)` from its reader
21986    // for a stream closed WITH a close_notify and
21987    // `UnexpectedEof("peer closed connection without sending TLS
21988    // close_notify")` for one closed without, so only the alert lets a peer
21989    // tell a complete response from a connection cut mid-stream. A test that
21990    // asserted a FIN had arrived would pass against the no-op mutant, which
21991    // still closes the socket.
21992
21993    /// The frontend idle timeout on a server H2 connection makes the peer
21994    /// receive a TLS close_notify alert.
21995    ///
21996    /// The route is the production one and the whole of it:
21997    /// `Mux::timeout` -> `Mux::delay_close_for_frontend_flush` ->
21998    /// `Connection::initiate_close_notify` ->
21999    /// `H2Shell::initiate_close_notify` -> `begin_tls_close` ->
22000    /// `FrontRustls::socket_close` -> `send_close_notify`.
22001    ///
22002    /// `Mux::delay_close_for_frontend_flush` is the SOLE caller of
22003    /// `Connection::initiate_close_notify`, and it is itself called from four
22004    /// sites in `Mux::ready_inner` and this one in `Mux::timeout_inner`.
22005    /// `Mux::shutting_down` is not among them: it drives frontend I/O and can
22006    /// flush records the alert has already generated, but it never asks for
22007    /// one. The timeout is chosen over the four `ready_inner` sites only
22008    /// because they need a `dyn ProxySession` and a `dyn L7Proxy`; the seam
22009    /// under test is the same on all five.
22010    ///
22011    /// The flush is `Connection::flush_output_buffer` — the same call
22012    /// `Mux::shutting_down_inner` makes for exactly this purpose, on a
22013    /// connection whose output queue is empty so nothing but the alert reaches
22014    /// the wire. `FrontRustls::socket_write(&[])` takes its "flush pending TLS
22015    /// records even if no application data was written" block, which is what
22016    /// hands the record to `write_tls`.
22017    ///
22018    /// TO SEE THIS RED: replace the body of `H2Shell::begin_tls_close`
22019    /// with `{}`. rustls is then never asked for the alert, `tls_wants_write()`
22020    /// answers false, `initiate_close_notify` returns false and the flush has
22021    /// nothing to push, so this test fails on `the peer must decrypt a TLS
22022    /// close_notify alert`. Measured: `1114 passed; 1 failed` — this test and
22023    /// nothing else, which is what a hole this complete looks like once it has
22024    /// exactly one witness. Note `close_notify_sent` is still set by the
22025    /// mutant, so no assertion on that flag could discriminate — only the
22026    /// bytes the peer receives can.
22027    #[test]
22028    fn a_frontend_timeout_makes_the_peer_receive_a_tls_close_notify() {
22029        use std::io::Read as _;
22030
22031        use crate::{StateResult, protocol::SessionState};
22032
22033        let pool = make_pool_for_invariant_16();
22034        let (mut connection, mut peer, mut client) = rustls_h2_connection(&pool, H2State::Header);
22035        // `Mux::consume_timer_entry` re-validates the wheel entry against the
22036        // core's own deadline and returns `Continue` having done NOTHING when
22037        // that deadline is still in the future. `ConnectionH2::new` arms one a
22038        // full `timeout_duration` out, so move it into the past: this is the
22039        // idle deadline genuinely elapsing, which is what the event loop would
22040        // be reacting to.
22041        connection.core.timeout_deadline = Some(Instant::now() - Duration::from_secs(60));
22042
22043        let frontend_token = mio::Token(0);
22044        let mut mux = crate::protocol::mux::Mux {
22045            configured_frontend_timeout: Duration::from_secs(30),
22046            frontend_token,
22047            frontend: crate::protocol::mux::Connection::H2(connection),
22048            router: Router::new(Duration::from_secs(30), Duration::from_secs(30)),
22049            // No streams at all, so the per-stream loop leaves `should_close`
22050            // true and `should_write` false, which is the only combination
22051            // that reaches `delay_close_for_frontend_flush`.
22052            context: test_context(&pool),
22053            session_ulid: Ulid::generate(),
22054            timeouts: crate::protocol::mux::MuxTimeouts::new(
22055                crate::timer::TimeoutContainer::new_empty(Duration::from_secs(30)),
22056            ),
22057            backend_registry: crate::protocol::mux::BackendRegistry::default(),
22058            backends: Rc::default(),
22059        };
22060        let mut metrics = SessionMetrics::new(None);
22061
22062        {
22063            let crate::protocol::mux::Connection::H2(h2) = &mux.frontend else {
22064                unreachable!("the frontend was built as H2")
22065            };
22066            assert!(
22067                !h2.socket.socket_wants_write(),
22068                "premise: the fixture must start with rustls holding nothing, \
22069                 or the records this test attributes to the alert could be the \
22070                 handshake's"
22071            );
22072        }
22073        assert!(
22074            !client
22075                .process_new_packets()
22076                .expect("the peer must decrypt what the frontend wrote")
22077                .peer_has_closed(),
22078            "premise: the peer must not have seen a close before the timeout \
22079             runs, or the closing assertion is reading a state it inherited"
22080        );
22081
22082        let result = mux.timeout(frontend_token, &mut metrics);
22083
22084        let mut received = Vec::new();
22085        let mut peer_saw_close_notify = false;
22086        for _ in 0..MAX_DRIVE_TICKS {
22087            mux.frontend.flush_output_buffer();
22088            // Loopback delivery is softirq-driven, not synchronous with
22089            // `write_tls`: give it a slot rather than spinning the CPU.
22090            std::thread::yield_now();
22091            drain_peer(&mut client, &mut peer, &mut received);
22092            peer_saw_close_notify = client
22093                .process_new_packets()
22094                .expect("the peer must decrypt what the frontend wrote")
22095                .peer_has_closed();
22096            if peer_saw_close_notify {
22097                break;
22098            }
22099        }
22100
22101        assert!(
22102            peer_saw_close_notify,
22103            "the peer must decrypt a TLS close_notify alert: without it a \
22104             client cannot tell this orderly shutdown from a stream truncated \
22105             mid-response, and reports a protocol error instead of a clean end"
22106        );
22107        let mut sentinel = [0u8; 1];
22108        assert!(
22109            matches!(client.reader().read(&mut sentinel), Ok(0)),
22110            "the alert must be what ended the peer's stream, stated in \
22111             rustls's own vocabulary: its reader answers Ok(0) only for a \
22112             stream closed with a close_notify, UnexpectedEof for a bare FIN \
22113             and WouldBlock for neither — so this is the assertion a FIN \
22114             cannot satisfy"
22115        );
22116        assert!(
22117            received.is_empty(),
22118            "nothing but the alert may reach the peer on this path: decrypted \
22119             plaintext here would mean the connection also wrote a frame after \
22120             asking rustls to close, which RFC 8446 §6.1 has the peer discard \
22121             ({} bytes)",
22122            received.len()
22123        );
22124        assert_eq!(
22125            result,
22126            StateResult::Continue,
22127            "a timeout that queued an alert must keep the session alive until \
22128             the records drain: closing here sends FIN over the very bytes the \
22129             close_notify was generated to deliver"
22130        );
22131    }
22132
22133    // ── Connection-level idle deadline: what counts as liveness ────────
22134    //
22135    // LIFECYCLE §9 invariant 9 says the connection timer resets only on
22136    // application activity, and `ConnectionH2::poll_read_target` spells out
22137    // the reason in prose: a peer sending periodic PINGs must not prevent
22138    // timeout detection on a stuck session. The READ side honoured it. The
22139    // WRITE side did not, because `H2Shell::write_streams` armed at pass
22140    // entry and `writable()` reaches that function on every proxying-state
22141    // pass — including the pass whose only output was the PING or SETTINGS
22142    // acknowledgement `flush_pending_control_frames` had just drained. So the
22143    // rule held in one direction and was undone in the other, for exactly the
22144    // frame types it names (sozu-proxy/sozu#1489).
22145    //
22146    // Every test below pairs the negative with a POSITIVE leg on the same
22147    // connection. A test that only asserted "the deadline did not move" would
22148    // pass just as well against a build that never armed at all, which is the
22149    // opposite defect and a worse one: it turns the frontend timeout into an
22150    // unconditional session cap.
22151
22152    /// Long enough that the per-stream idle deadline — a separate mechanism,
22153    /// deliberately untouched here — cannot retire the registered stream part
22154    /// way through a measurement and change what the later rows are measuring.
22155    const LIVENESS_TIMEOUT: Duration = Duration::from_secs(600);
22156
22157    /// A server connection whose frontend timeout and per-stream idle cap are
22158    /// both [`LIVENESS_TIMEOUT`], with ONE registered stream holding no queued
22159    /// response yet, plus the pass-zero instant every assertion below is
22160    /// expressed against.
22161    ///
22162    /// The stream is registered directly rather than opened with a HEADERS
22163    /// frame, and it starts EMPTY on purpose: a stream with queued output
22164    /// would let the write pass that flushes a PING ACK also flush that
22165    /// output, and arm the deadline for a reason the negative leg is not
22166    /// trying to measure.
22167    fn liveness_fixture(
22168        pool: &Rc<RefCell<Pool>>,
22169    ) -> (
22170        H2Shell<BackpressuredTlsSocket>,
22171        Context<TestListener>,
22172        GlobalStreamId,
22173        Instant,
22174        std::net::TcpStream,
22175    ) {
22176        let (mut connection, peer) = connection_with_backpressure(pool, 0, 0, H2State::Header);
22177        let t0 = connection.core.now;
22178        connection.core.timeout_duration = LIVENESS_TIMEOUT;
22179        connection.core.stream_idle_timeout = LIVENESS_TIMEOUT;
22180        connection.core.timeout_deadline = t0.checked_add(LIVENESS_TIMEOUT);
22181        connection
22182            .core
22183            .stream_table
22184            .set_expect_read(Some((H2StreamId::Zero, 9)));
22185        let mut context = test_context(pool);
22186        let gid = context
22187            .create_stream(Ulid::generate(), 1 << 16)
22188            .expect("test context must create a stream");
22189        connection
22190            .core
22191            .stream_table
22192            .register(REGISTERED_STREAM_ID, gid, connection.core.now);
22193        (connection, context, gid, t0, peer)
22194    }
22195
22196    /// A non-ACK PING. `ConnectionH2::handle_ping_frame` answers it by
22197    /// serialising a PING ACK into `self.zero` and parking
22198    /// `expect_write(Some(H2StreamId::Zero))`, which is what gives the write
22199    /// path something to send.
22200    fn liveness_ping_frame() -> Vec<u8> {
22201        let mut frame = vec![0, 0, 8, 6, 0, 0, 0, 0, 0];
22202        frame.extend_from_slice(&[1, 2, 3, 4, 5, 6, 7, 8]);
22203        frame
22204    }
22205
22206    /// A non-ACK SETTINGS carrying no entry.
22207    /// `ConnectionH2::handle_settings_frame` answers it with
22208    /// `serializer::SETTINGS_ACKNOWLEDGEMENT`, parking the same way.
22209    fn liveness_settings_frame() -> Vec<u8> {
22210        vec![0, 0, 0, 4, 0, 0, 0, 0, 0]
22211    }
22212
22213    /// A connection-level WINDOW_UPDATE. The control frame that queues NO
22214    /// acknowledgement, and therefore the one the pre-image already handled
22215    /// correctly — it is here so the fix is shown not to have moved it.
22216    fn liveness_window_update_frame() -> Vec<u8> {
22217        let mut frame = vec![0, 0, 4, 8, 0, 0, 0, 0, 0];
22218        frame.extend_from_slice(&1024u32.to_be_bytes());
22219        frame
22220    }
22221
22222    /// A HEADERS frame opening `stream_id`, END_STREAM | END_HEADERS.
22223    fn liveness_headers_frame(stream_id: u32) -> Vec<u8> {
22224        let mut encoder = crate::protocol::mux::hpack::Encoder::new();
22225        let block = encoder.encode([
22226            (&b":method"[..], &b"GET"[..]),
22227            (&b":scheme"[..], &b"https"[..]),
22228            (&b":authority"[..], &b"example.com"[..]),
22229            (&b":path"[..], &b"/"[..]),
22230        ]);
22231        let mut frame = Vec::with_capacity(9 + block.len());
22232        frame.extend_from_slice(&(block.len() as u32).to_be_bytes()[1..]);
22233        frame.push(1);
22234        frame.push(parser::FLAG_END_STREAM | parser::FLAG_END_HEADERS);
22235        frame.extend_from_slice(&stream_id.to_be_bytes());
22236        frame.extend_from_slice(&block);
22237        frame
22238    }
22239
22240    /// Spin until `n` bytes the peer wrote have reached the connection's
22241    /// socket.
22242    ///
22243    /// Not belt-and-braces: without it the read loop below can quiesce on a
22244    /// `WouldBlock` that only means "not yet", and every assertion downstream
22245    /// then measures a frame that was never delivered — which is a test that
22246    /// passes for the wrong reason, in the direction of the negative legs.
22247    fn liveness_await_bytes(connection: &H2Shell<BackpressuredTlsSocket>, n: usize) {
22248        let mut buf = vec![0u8; n];
22249        for _ in 0..1_000_000 {
22250            if let Ok(got) = connection.socket.socket_ref().peek(&mut buf)
22251                && got >= n
22252            {
22253                return;
22254            }
22255            std::thread::yield_now();
22256        }
22257        panic!("the peer's {n} bytes never reached the connection socket");
22258    }
22259
22260    /// Deliver `frame` at `at` and run the READ half only, the way
22261    /// `Mux::ready_inner` runs it: `readable()` while `filter_interest()` is
22262    /// readable.
22263    ///
22264    /// Stopping before the write half is what lets a test assert the premise —
22265    /// that the acknowledgement really was queued — instead of inferring it
22266    /// from the deadline it is about to measure.
22267    fn liveness_read(
22268        connection: &mut H2Shell<BackpressuredTlsSocket>,
22269        context: &mut Context<TestListener>,
22270        router: &mut Router,
22271        peer: &mut std::net::TcpStream,
22272        at: Instant,
22273        frame: &[u8],
22274    ) {
22275        context.now = at;
22276        peer.write_all(frame).expect("loopback write must complete");
22277        peer.flush().expect("loopback flush must complete");
22278        liveness_await_bytes(connection, frame.len());
22279        connection.core.readiness.interest.insert(Ready::READABLE);
22280        connection.core.readiness.event.insert(Ready::READABLE);
22281        for _ in 0..64 {
22282            if !connection.core.readiness.filter_interest().is_readable() {
22283                let mut leftover = [0u8; 1];
22284                assert!(
22285                    !matches!(connection.socket.socket_ref().peek(&mut leftover), Ok(1)),
22286                    "the read half stopped with the frame still unread, so \
22287                     nothing below is measuring the frame it names"
22288                );
22289                return;
22290            }
22291            connection.readable(context, EndpointClient(&mut *router));
22292        }
22293        panic!(
22294            "the read half never settled: {:?}",
22295            connection.core.readiness
22296        );
22297    }
22298
22299    /// Run the WRITE half at the clock the read half left: `writable()` while
22300    /// `filter_interest()` is writable. This is the pass that used to arm the
22301    /// frontend deadline unconditionally.
22302    fn liveness_write(
22303        connection: &mut H2Shell<BackpressuredTlsSocket>,
22304        context: &mut Context<TestListener>,
22305        router: &mut Router,
22306    ) {
22307        for _ in 0..64 {
22308            if !connection.core.readiness.filter_interest().is_writable() {
22309                return;
22310            }
22311            connection.writable(context, EndpointClient(&mut *router));
22312        }
22313        panic!(
22314            "the write half never settled: {:?}",
22315            connection.core.readiness
22316        );
22317    }
22318
22319    /// Queue `blocks` as the registered stream's response and drive ONE
22320    /// `writable()` pass at `at`. Returns the bytes that reached the socket
22321    /// across the whole connection's lifetime so far.
22322    fn liveness_flush_stream(
22323        connection: &mut H2Shell<BackpressuredTlsSocket>,
22324        context: &mut Context<TestListener>,
22325        router: &mut Router,
22326        gid: GlobalStreamId,
22327        at: Instant,
22328        blocks: &[&'static [u8]],
22329    ) -> usize {
22330        for block in blocks {
22331            context.streams[gid]
22332                .back
22333                .out
22334                .push_back(kawa::OutBlock::Store(kawa::Store::Static(block)));
22335        }
22336        context.now = at;
22337        connection.core.readiness.interest.insert(Ready::WRITABLE);
22338        connection.core.readiness.event.insert(Ready::WRITABLE);
22339        connection.writable(context, EndpointClient(&mut *router));
22340        context.streams[gid].metrics.bout
22341    }
22342
22343    /// The deadline a connection publishes, as an offset from pass zero, so a
22344    /// failure reads in the units the issue's table is written in.
22345    fn liveness_deadline(connection: &H2Shell<BackpressuredTlsSocket>, t0: Instant) -> Duration {
22346        connection
22347            .core
22348            .poll_timeout()
22349            .expect("a live H2 connection must publish a deadline")
22350            .saturating_duration_since(t0)
22351    }
22352
22353    /// A peer that sends nothing but PINGs must not hold a stuck session open.
22354    ///
22355    /// The PING is answered — `handle_ping_frame` parks the ACK on
22356    /// `expect_write` and arms WRITABLE — so the write path genuinely runs and
22357    /// genuinely has bytes to emit. What it must not do is read that as the
22358    /// peer being alive on a stream: the ACK is the proxy talking to itself.
22359    ///
22360    /// TO SEE THIS RED: put `self.arm_timeout();` back as the first statement
22361    /// of `H2Shell::write_streams`. Measured on that revert, this test
22362    /// fails on `a PING is answered but is not stream activity`, with
22363    /// `left: 610s, right: 600.01s` — the deadline follows the PING instead of
22364    /// staying where the request left it. It is one of FOUR failures, the
22365    /// siblings being the three tests below; every other test in the suite
22366    /// passes.
22367    ///
22368    /// Neutering the OTHER half instead — `if size > 0` to `if false` in
22369    /// `ConnectionH2::handle_write` — reddens this test too, on its positive
22370    /// leg (`left: 600.01s, right: 1130s`). That is the pair that makes it a
22371    /// measurement: neither a build that always arms nor a build that never
22372    /// arms can satisfy it.
22373    #[test]
22374    fn a_ping_only_peer_does_not_postpone_the_connection_deadline() {
22375        let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 8, 16384)));
22376        let (mut connection, mut context, gid, t0, mut peer) = liveness_fixture(&pool);
22377        let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
22378
22379        assert_eq!(
22380            liveness_deadline(&connection, t0),
22381            LIVENESS_TIMEOUT,
22382            "premise: the connection starts one full timeout out from pass zero"
22383        );
22384
22385        // The request. The READ side already honoured the rule, and this leg
22386        // is the control for it: a build that stopped arming on HEADERS would
22387        // make every "did not move" assertion below true for the wrong reason.
22388        let request_at = Duration::from_millis(10);
22389        liveness_read(
22390            &mut connection,
22391            &mut context,
22392            &mut router,
22393            &mut peer,
22394            t0 + request_at,
22395            &liveness_headers_frame(3),
22396        );
22397        liveness_write(&mut connection, &mut context, &mut router);
22398        assert_eq!(
22399            liveness_deadline(&connection, t0),
22400            request_at + LIVENESS_TIMEOUT,
22401            "HEADERS is application activity and must arm the deadline: \
22402             without this leg the assertions below cannot tell a fix from a \
22403             build that never arms at all"
22404        );
22405
22406        // The defect. Two PINGs, 490 seconds apart, either of which used to
22407        // renew the session on its own.
22408        for ping_at in [Duration::from_secs(10), Duration::from_secs(500)] {
22409            liveness_read(
22410                &mut connection,
22411                &mut context,
22412                &mut router,
22413                &mut peer,
22414                t0 + ping_at,
22415                &liveness_ping_frame(),
22416            );
22417            assert!(
22418                !connection.core.output.is_empty(),
22419                "premise: the PING must really have been answered and queued, \
22420                 so the write pass below is the ACK-only pass this test is \
22421                 about and not a no-op"
22422            );
22423
22424            liveness_write(&mut connection, &mut context, &mut router);
22425
22426            assert!(
22427                connection.core.output.is_empty(),
22428                "premise: the ACK flush must have completed, which is what \
22429                 lets `flush_pending_control_frames` answer Proceed and fall \
22430                 through into the proxying-state write pass"
22431            );
22432            assert_eq!(
22433                liveness_deadline(&connection, t0),
22434                request_at + LIVENESS_TIMEOUT,
22435                "a PING is answered but is not stream activity: the deadline \
22436                 must stay where the request left it, or a peer sending \
22437                 periodic PINGs holds a stuck session open indefinitely"
22438            );
22439        }
22440
22441        // A WINDOW_UPDATE queues no acknowledgement, so the pre-image already
22442        // left it alone. Pinned so the fix is shown not to have moved it.
22443        liveness_read(
22444            &mut connection,
22445            &mut context,
22446            &mut router,
22447            &mut peer,
22448            t0 + Duration::from_secs(520),
22449            &liveness_window_update_frame(),
22450        );
22451        liveness_write(&mut connection, &mut context, &mut router);
22452        assert_eq!(
22453            liveness_deadline(&connection, t0),
22454            request_at + LIVENESS_TIMEOUT,
22455            "a WINDOW_UPDATE queues nothing and must not arm the deadline"
22456        );
22457
22458        // The positive leg, on the same connection: outbound application data
22459        // IS activity. Without it every assertion above is satisfied by a
22460        // build that never arms the deadline at all.
22461        let response_at = Duration::from_secs(530);
22462        let written = liveness_flush_stream(
22463            &mut connection,
22464            &mut context,
22465            &mut router,
22466            gid,
22467            t0 + response_at,
22468            &[FIRST_BLOCK, SECOND_BLOCK],
22469        );
22470        assert_eq!(
22471            written, TOTAL_QUEUED,
22472            "premise for the positive leg: the pass must really have moved the \
22473             response bytes, or the deadline below moved without stream data"
22474        );
22475        assert_eq!(
22476            liveness_deadline(&connection, t0),
22477            response_at + LIVENESS_TIMEOUT,
22478            "a write pass that moved a stream's bytes IS liveness and must arm \
22479             the deadline: the fix withdraws arming from acknowledgement-only \
22480             passes, not from the write path"
22481        );
22482    }
22483
22484    /// The same contract for SETTINGS, the other control frame that queues an
22485    /// acknowledgement.
22486    ///
22487    /// Not a duplicate of the PING test: `handle_settings_frame` reaches the
22488    /// park through an entirely different body — the per-identifier apply
22489    /// loop, the `pending_table_size_update` signal and
22490    /// `serializer::SETTINGS_ACKNOWLEDGEMENT` rather than
22491    /// `serializer::gen_ping_acknowledgement` — and the issue's table measured
22492    /// both moving independently.
22493    ///
22494    /// TO SEE THIS RED: put `self.arm_timeout();` back as the first statement
22495    /// of `H2Shell::write_streams`. Measured on that revert, this test
22496    /// fails on `a SETTINGS ACK is not stream activity`, with
22497    /// `left: 640s, right: 600.01s`. Neutering the other half instead —
22498    /// `if size > 0` to `if false` in `ConnectionH2::handle_write` — reddens
22499    /// it on its positive leg, `left: 600.01s, right: 650s`.
22500    #[test]
22501    fn a_settings_only_peer_does_not_postpone_the_connection_deadline() {
22502        let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 8, 16384)));
22503        let (mut connection, mut context, gid, t0, mut peer) = liveness_fixture(&pool);
22504        let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
22505
22506        let request_at = Duration::from_millis(10);
22507        liveness_read(
22508            &mut connection,
22509            &mut context,
22510            &mut router,
22511            &mut peer,
22512            t0 + request_at,
22513            &liveness_headers_frame(3),
22514        );
22515        liveness_write(&mut connection, &mut context, &mut router);
22516        assert_eq!(
22517            liveness_deadline(&connection, t0),
22518            request_at + LIVENESS_TIMEOUT,
22519            "premise: the request armed the deadline, so a later move is \
22520             attributable to the frame that caused it"
22521        );
22522
22523        liveness_read(
22524            &mut connection,
22525            &mut context,
22526            &mut router,
22527            &mut peer,
22528            t0 + Duration::from_secs(40),
22529            &liveness_settings_frame(),
22530        );
22531        assert!(
22532            !connection.core.output.is_empty(),
22533            "premise: the SETTINGS must really have been acknowledged and \
22534             queued, so the write pass below is the ACK-only pass"
22535        );
22536
22537        liveness_write(&mut connection, &mut context, &mut router);
22538
22539        assert!(
22540            connection.core.output.is_empty(),
22541            "premise: the ACK flush must have completed and fallen through \
22542             into the proxying-state write pass"
22543        );
22544        assert_eq!(
22545            liveness_deadline(&connection, t0),
22546            request_at + LIVENESS_TIMEOUT,
22547            "a SETTINGS ACK is not stream activity: the deadline must stay \
22548             where the request left it"
22549        );
22550
22551        let response_at = Duration::from_secs(50);
22552        let written = liveness_flush_stream(
22553            &mut connection,
22554            &mut context,
22555            &mut router,
22556            gid,
22557            t0 + response_at,
22558            &[FIRST_BLOCK, SECOND_BLOCK],
22559        );
22560        assert_eq!(
22561            written, TOTAL_QUEUED,
22562            "premise for the positive leg: the pass must really have moved the \
22563             response bytes"
22564        );
22565        assert_eq!(
22566            liveness_deadline(&connection, t0),
22567            response_at + LIVENESS_TIMEOUT,
22568            "outbound application data must still arm the deadline"
22569        );
22570    }
22571
22572    /// A write pass that emits a stream's bytes arms the deadline; the pass
22573    /// before it, which had nothing queued, does not.
22574    ///
22575    /// The PING and SETTINGS tests above each carry a positive leg, so this
22576    /// one is not the only guard against a build that never arms. What it adds
22577    /// is the pair measured on ONE connection with NOTHING but the queued
22578    /// response changing between the two passes: no frame is read, so neither
22579    /// `handle_headers_frame` nor `poll_read_target`'s DATA arm can be what
22580    /// moved the deadline, and the write path is the only remaining candidate.
22581    ///
22582    /// TO SEE THIS RED: change the `if size > 0` gate in
22583    /// `ConnectionH2::handle_write` to `if false`. The empty-pass assertion
22584    /// still holds and this test then fails on `a write pass that moved a
22585    /// stream's bytes must arm the deadline`, with `left: 600s, right: 620s`.
22586    /// Restoring `self.arm_timeout()` to the top of
22587    /// `H2Shell::write_streams` reddens the OTHER assertion instead, `a
22588    /// write pass that emitted nothing is not liveness`, with
22589    /// `left: 610s, right: 600s`.
22590    #[test]
22591    fn a_write_pass_that_moves_stream_bytes_arms_the_connection_deadline() {
22592        let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 8, 16384)));
22593        let (mut connection, mut context, gid, t0, _peer) = liveness_fixture(&pool);
22594        let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
22595
22596        // A write pass with nothing queued: the control that makes the
22597        // assertion below a measurement of the bytes and not of the pass.
22598        let empty_at = Duration::from_secs(10);
22599        let written = liveness_flush_stream(
22600            &mut connection,
22601            &mut context,
22602            &mut router,
22603            gid,
22604            t0 + empty_at,
22605            &[],
22606        );
22607        assert_eq!(
22608            written, 0,
22609            "premise: a pass with nothing queued moves no byte"
22610        );
22611        assert_eq!(
22612            liveness_deadline(&connection, t0),
22613            LIVENESS_TIMEOUT,
22614            "a write pass that emitted nothing is not liveness"
22615        );
22616
22617        let response_at = Duration::from_secs(20);
22618        let written = liveness_flush_stream(
22619            &mut connection,
22620            &mut context,
22621            &mut router,
22622            gid,
22623            t0 + response_at,
22624            &[FIRST_BLOCK, SECOND_BLOCK],
22625        );
22626        assert_eq!(
22627            written, TOTAL_QUEUED,
22628            "premise: the pass must really have moved the response bytes"
22629        );
22630        assert_eq!(
22631            liveness_deadline(&connection, t0),
22632            response_at + LIVENESS_TIMEOUT,
22633            "a write pass that moved a stream's bytes must arm the deadline"
22634        );
22635    }
22636
22637    /// Draining changes nothing about what counts as liveness.
22638    ///
22639    /// `Mux::drive_frontend_shutdown_io` sets `force_h2_write` unconditionally
22640    /// for an H2 frontend, so a draining connection still in `H2State::Header`
22641    /// reaches `H2Shell::write_streams` on every `shutting_down()` poll —
22642    /// which is why arming at pass entry made a draining session immune to the
22643    /// frontend timeout for the whole drain, whether or not a byte moved. The
22644    /// drain keeps its deadline the same way every other pass does: by
22645    /// flushing a stream's bytes.
22646    ///
22647    /// TO SEE THIS RED: change the `if size > 0` gate in
22648    /// `ConnectionH2::handle_write` to `if false`. This test then fails on `a
22649    /// draining pass that flushed a stream's bytes must arm the deadline`,
22650    /// with `left: 600s, right: 620s`. Restoring `self.arm_timeout()` to the
22651    /// top of `H2Shell::write_streams` reddens the other assertion, `a
22652    /// draining pass that emitted nothing is not liveness either`, with
22653    /// `left: 610s, right: 600s`.
22654    #[test]
22655    fn a_draining_write_pass_that_moves_stream_bytes_still_arms_the_deadline() {
22656        let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 8, 16384)));
22657        let (mut connection, mut context, gid, t0, _peer) = liveness_fixture(&pool);
22658        let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
22659        connection.core.drain.__test_set_draining();
22660
22661        let empty_at = Duration::from_secs(10);
22662        let written = liveness_flush_stream(
22663            &mut connection,
22664            &mut context,
22665            &mut router,
22666            gid,
22667            t0 + empty_at,
22668            &[],
22669        );
22670        assert_eq!(
22671            written, 0,
22672            "premise: a draining pass with nothing queued moves no byte"
22673        );
22674        assert_eq!(
22675            liveness_deadline(&connection, t0),
22676            LIVENESS_TIMEOUT,
22677            "a draining pass that emitted nothing is not liveness either: \
22678             `drive_frontend_shutdown_io` forces a write pass on every \
22679             shutting-down poll, so arming here renews the session for free"
22680        );
22681
22682        let response_at = Duration::from_secs(20);
22683        let written = liveness_flush_stream(
22684            &mut connection,
22685            &mut context,
22686            &mut router,
22687            gid,
22688            t0 + response_at,
22689            &[FIRST_BLOCK, SECOND_BLOCK],
22690        );
22691        assert_eq!(
22692            written, TOTAL_QUEUED,
22693            "premise: the draining pass must really have moved the response \
22694             bytes, so the drain has time to finish delivering them"
22695        );
22696        assert_eq!(
22697            liveness_deadline(&connection, t0),
22698            response_at + LIVENESS_TIMEOUT,
22699            "a draining pass that flushed a stream's bytes must arm the \
22700             deadline: the drain has to be able to finish delivering a \
22701             response the peer is still reading"
22702        );
22703    }
22704    // ── LIFECYCLE §9 invariant 14 — the backend accounting ledger ────────
22705    //
22706    // Invariant 14 is a balance: every stream removed outside
22707    // `Connection::end_stream` must release the `active_requests` charge its
22708    // creation took. Before #1340's Question 12 that balance was spread over
22709    // six sites, each reaching a `Rc<RefCell<Backend>>` through
22710    // `Position::Client` and mutating it in place, and nothing could observe
22711    // the pair — a missed decrement showed up as load-balancing drift in
22712    // production, days later.
22713    //
22714    // The core now emits `BackendDelta`s and the embedder performs them, so
22715    // the balance is a property of one observable list. These tests read that
22716    // list directly, and then apply it to a real `Backend` and assert the
22717    // counter returns to where it started, which is the invariant itself
22718    // rather than a proxy for it.
22719
22720    /// Sum a ledger into the net `active_requests` change it describes, and
22721    /// the number of connection releases it carries.
22722    fn reconcile(deltas: &[BackendDelta]) -> (i64, usize) {
22723        let mut net = 0i64;
22724        let mut closed = 0usize;
22725        for delta in deltas {
22726            match delta.change {
22727                BackendChange::StreamsStarted(count) => net += count as i64,
22728                BackendChange::StreamsEnded(count) => net -= count as i64,
22729                BackendChange::ConnectionClosed => closed += 1,
22730                // Only `Mux::dial_backend` emits it, and no scenario here
22731                // dials: an H2 connection's own paths never release a
22732                // reservation.
22733                BackendChange::DialFailed(_) => {
22734                    unreachable!("no invariant-14 scenario dials a backend")
22735                }
22736            }
22737        }
22738        (net, closed)
22739    }
22740
22741    /// Everything one of these scenarios needs: a live backend connection in
22742    /// `Connected`, the registry entry behind it, and the peer that keeps the
22743    /// loopback socket open.
22744    struct LedgerFixture {
22745        connection: Connection<SessionTcpStream>,
22746        registry: crate::protocol::mux::BackendRegistry,
22747        backend: Rc<RefCell<crate::backends::Backend>>,
22748        _peer: std::net::TcpStream,
22749    }
22750
22751    fn ledger_fixture(pool: &Rc<RefCell<Pool>>) -> LedgerFixture {
22752        let (socket, peer) = connected_socket();
22753        let address: std::net::SocketAddr =
22754            "127.0.0.1:2".parse().expect("backend address must parse");
22755        let backend = Rc::new(RefCell::new(crate::backends::Backend::new(
22756            "ledger-backend",
22757            address,
22758            None,
22759            None,
22760            None,
22761        )));
22762        let mut registry = crate::protocol::mux::BackendRegistry::default();
22763        let backend_id = registry.id_for(&backend);
22764        let session_ulid = Ulid::generate();
22765        let mut connection = Connection::new_h2_client(
22766            session_ulid,
22767            SessionTcpStream::new(socket, session_ulid, Some(address)),
22768            "ledger-cluster".into(),
22769            backend_id,
22770            &mut PoolBufferSource::new(Rc::downgrade(pool)),
22771            Duration::from_secs(30),
22772            H2FloodConfig::default(),
22773            H2ConnectionConfig::default(),
22774            Duration::from_secs(30),
22775            None,
22776        )
22777        .expect("the test pool must hand out a buffer for the H2 backend");
22778        // `new_h2_client` opens in `Connecting`, where the charge is taken by
22779        // the embedder at the transition rather than by `start_stream`. Move
22780        // it the way a completed dial does, so these scenarios exercise the
22781        // `Connected` guard the three reset paths read.
22782        if let Position::Client(_, _, status) = connection.position_mut() {
22783            *status = BackendStatus::Connected;
22784        }
22785        LedgerFixture {
22786            connection,
22787            registry,
22788            backend,
22789            _peer: peer,
22790        }
22791    }
22792
22793    /// Open `count` streams on the backend through the production path, and
22794    /// return their global ids.
22795    fn open_streams(
22796        fixture: &mut LedgerFixture,
22797        context: &mut Context<TestListener>,
22798        count: usize,
22799    ) -> Vec<GlobalStreamId> {
22800        (0..count)
22801            .map(|_| {
22802                let gid = context
22803                    .create_stream(Ulid::generate(), 65_535)
22804                    .expect("the test pool must hand out stream buffers");
22805                assert!(
22806                    fixture.connection.start_stream(gid, context),
22807                    "the backend must accept the stream this scenario opens"
22808                );
22809                gid
22810            })
22811            .collect()
22812    }
22813
22814    /// Apply a ledger and read the counter it leaves behind.
22815    fn settle(fixture: &mut LedgerFixture, context: &mut Context<TestListener>) -> usize {
22816        let backends = RefCell::new(crate::backends::BackendMap::with_seed(0));
22817        for delta in std::mem::take(&mut context.backend_deltas) {
22818            fixture.registry.apply(delta, &backends);
22819        }
22820        fixture.backend.borrow().active_requests
22821    }
22822
22823    /// The nominal path: what `Connection::start_stream` charges,
22824    /// `Connection::end_stream` releases, and the close releases the
22825    /// connection slot exactly once.
22826    #[test]
22827    fn invariant_14_start_and_end_stream_balance() {
22828        let pool = make_pool_for_invariant_16();
22829        let mut context = test_context(&pool);
22830        let mut fixture = ledger_fixture(&pool);
22831        let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
22832
22833        let gids = open_streams(&mut fixture, &mut context, 3);
22834        assert_eq!(
22835            reconcile(&context.backend_deltas),
22836            (3, 0),
22837            "three started streams must charge exactly three"
22838        );
22839
22840        for gid in gids {
22841            fixture.connection.end_stream(gid, &mut context);
22842        }
22843        fixture
22844            .connection
22845            .close(&mut context, EndpointClient(&mut router));
22846
22847        assert_eq!(
22848            reconcile(&context.backend_deltas),
22849            (0, 1),
22850            "every charge must be released, and the connection released once"
22851        );
22852        assert_eq!(
22853            settle(&mut fixture, &mut context),
22854            0,
22855            "applying the ledger must return active_requests to zero"
22856        );
22857    }
22858
22859    /// A refused `start_stream` must charge nothing at all — not charge and
22860    /// then correctly undo. The rollback that used to sit here is the shape
22861    /// this asserts is gone: a ledger with a `+1` and a `-1` in it would also
22862    /// reconcile to zero, so the assertion is on the list being EMPTY.
22863    #[test]
22864    fn invariant_14_a_refused_start_stream_charges_nothing() {
22865        let pool = make_pool_for_invariant_16();
22866        let mut context = test_context(&pool);
22867        let mut fixture = ledger_fixture(&pool);
22868
22869        // Drive the peer's MAX_CONCURRENT_STREAMS to zero so the very next
22870        // `start_stream` is refused by RFC 9113 §5.1.2's check.
22871        let Connection::H2(shell) = &mut fixture.connection else {
22872            unreachable!("the fixture was built as H2");
22873        };
22874        shell.core.peer_settings.settings_max_concurrent_streams = 0;
22875
22876        let gid = context
22877            .create_stream(Ulid::generate(), 65_535)
22878            .expect("the test pool must hand out stream buffers");
22879        assert!(
22880            !fixture.connection.start_stream(gid, &mut context),
22881            "premise: the backend must refuse this stream"
22882        );
22883
22884        assert!(
22885            context.backend_deltas.is_empty(),
22886            "a refused start_stream must leave no entry in the ledger at all, \
22887             not a charge and a compensating release: got {:?}",
22888            context.backend_deltas
22889        );
22890        assert_eq!(
22891            settle(&mut fixture, &mut context),
22892            0,
22893            "a refused start_stream must leave active_requests untouched"
22894        );
22895    }
22896
22897    /// An inbound `RST_STREAM` removes the stream without going through
22898    /// `Connection::end_stream`, so it owes the release itself.
22899    #[test]
22900    fn invariant_14_an_inbound_rst_stream_releases_its_charge() {
22901        let pool = make_pool_for_invariant_16();
22902        let mut context = test_context(&pool);
22903        let mut fixture = ledger_fixture(&pool);
22904        let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
22905
22906        let _gids = open_streams(&mut fixture, &mut context, 2);
22907        let Connection::H2(shell) = &mut fixture.connection else {
22908            unreachable!("the fixture was built as H2");
22909        };
22910        let reset_id = *shell
22911            .core
22912            .stream_table
22913            .streams()
22914            .keys()
22915            .next()
22916            .expect("the scenario opened two streams");
22917        shell.core.handle_rst_stream_frame(
22918            parser::RstStream {
22919                stream_id: reset_id,
22920                error_code: H2Error::Cancel as u32,
22921            },
22922            &mut context,
22923            EndpointClient(&mut router),
22924        );
22925
22926        assert_eq!(
22927            reconcile(&context.backend_deltas),
22928            (1, 0),
22929            "the reset stream must release its charge, and only its own"
22930        );
22931        assert_eq!(
22932            settle(&mut fixture, &mut context),
22933            1,
22934            "one of the two streams is gone, so exactly one charge remains"
22935        );
22936    }
22937
22938    /// On a backend connection (`Position::Client`) a `RST_STREAM` the backend
22939    /// sends before its response starts — typically `REFUSED_STREAM` under
22940    /// load — is not the Rapid Reset signature: Sōzu opened the stream and the
22941    /// reset costs it nothing. Three hundred of them, sixty at a time, must
22942    /// not trip the pre-response reset cap.
22943    ///
22944    /// TO SEE THIS RED: in `ConnectionH2::handle_rst_stream_frame`, drop the
22945    /// `self.position.is_client()` term from `response_started`. Every reset
22946    /// then counts as pre-response, and with one reset per opened stream the
22947    /// 251st crosses `h2_max_rst_stream_abusive_lifetime`.
22948    #[test]
22949    fn backend_resets_before_the_response_are_not_rapid_reset() {
22950        const ROUNDS: usize = 5;
22951        const PER_ROUND: usize = 60;
22952        let pool = Rc::new(RefCell::new(Pool::with_capacity(
22953            4,
22954            2 * ROUNDS * PER_ROUND + 8,
22955            16_384,
22956        )));
22957        let mut context = test_context(&pool);
22958        let mut fixture = ledger_fixture(&pool);
22959        let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
22960
22961        for round in 0..ROUNDS {
22962            let _gids = open_streams(&mut fixture, &mut context, PER_ROUND);
22963            let Connection::H2(shell) = &mut fixture.connection else {
22964                unreachable!("the fixture was built as H2");
22965            };
22966            let open: Vec<StreamId> = shell.core.stream_table.streams().keys().copied().collect();
22967            assert_eq!(open.len(), PER_ROUND, "round {round}: premise");
22968            for stream_id in open {
22969                let _ = shell.core.handle_rst_stream_frame(
22970                    parser::RstStream {
22971                        stream_id,
22972                        error_code: H2Error::RefusedStream as u32,
22973                    },
22974                    &mut context,
22975                    EndpointClient(&mut router),
22976                );
22977                assert!(
22978                    !matches!(shell.core.state, H2State::GoAway | H2State::Error),
22979                    "round {round}: backend reset of stream {stream_id} must not send GOAWAY"
22980                );
22981            }
22982        }
22983    }
22984
22985    // ── RST caps: what a frontend reset counts toward ───────────────────
22986
22987    /// A frontend H2 connection whose received-RST lifetime cap has the floor
22988    /// `lifetime_floor`, every other threshold at its default, so the
22989    /// scenarios below reach the cap in a handful of resets instead of
22990    /// 200 000.
22991    fn frontend_rst_fixture(
22992        lifetime_floor: u64,
22993    ) -> (
22994        Rc<RefCell<Pool>>,
22995        H2Shell<mio::net::TcpStream>,
22996        std::net::TcpStream,
22997        Context<TestListener>,
22998        Router,
22999    ) {
23000        let pool = Rc::new(RefCell::new(Pool::with_capacity(8, 512, 16_384)));
23001        let (mut connection, peer) = test_h2_connection(&pool, None);
23002        let config = H2FloodConfig::from_optional(
23003            None,
23004            None,
23005            None,
23006            None,
23007            None,
23008            None,
23009            None,
23010            Some(lifetime_floor),
23011            None,
23012            None,
23013            None,
23014            None,
23015            None,
23016            None,
23017        );
23018        connection.core.flood_detector =
23019            h2_flood_detector::H2FloodDetector::new(config, connection.core.now);
23020        let context = test_context(&pool);
23021        let router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
23022        (pool, connection, peer, context, router)
23023    }
23024
23025    /// Register client stream `stream_id` on `connection` as a stream routed
23026    /// to a backend, with its response started or not.
23027    fn open_routed_frontend_stream(
23028        connection: &mut H2Shell<mio::net::TcpStream>,
23029        context: &mut Context<TestListener>,
23030        stream_id: StreamId,
23031        response_started: bool,
23032    ) {
23033        let gid = context
23034            .create_stream(Ulid::generate(), 65_535)
23035            .expect("the test pool must hand out stream buffers");
23036        let stream = &mut context.streams[gid];
23037        stream.context.backend_id = Some(Rc::from("probe-backend"));
23038        if response_started {
23039            stream.back.parsing_phase = kawa::ParsingPhase::Body;
23040        }
23041        let now = connection.core.now;
23042        connection.core.stream_table.register(stream_id, gid, now);
23043    }
23044
23045    /// Feed one client `RST_STREAM(CANCEL)` for `stream_id` through
23046    /// `handle_rst_stream_frame`, and report whether the connection is now
23047    /// going away.
23048    fn client_cancels(
23049        connection: &mut H2Shell<mio::net::TcpStream>,
23050        context: &mut Context<TestListener>,
23051        router: &mut Router,
23052        stream_id: StreamId,
23053    ) -> bool {
23054        let _ = connection.core.handle_rst_stream_frame(
23055            parser::RstStream {
23056                stream_id,
23057                error_code: H2Error::Cancel as u32,
23058            },
23059            context,
23060            EndpointClient(router),
23061        );
23062        matches!(connection.core.state, H2State::GoAway | H2State::Error)
23063    }
23064
23065    /// A client that cancels every stream after its response started — a
23066    /// gRPC client abandoning server streams, a browser leaving a page while
23067    /// downloads run — resets exactly as many streams as a backend answered.
23068    /// The received-RST lifetime cap must never trip on it.
23069    ///
23070    /// TO SEE THIS RED: in `ConnectionH2::handle_rst_stream_frame`, count the
23071    /// reset stream with `record_stream_opened` after `record_rst_lifetime`
23072    /// instead of before. Each check then sees one more reset than answered
23073    /// streams, and the reset just past the floor sends GOAWAY.
23074    #[test]
23075    fn resets_after_the_response_never_trip_the_lifetime_cap() {
23076        let (_pool, mut connection, _peer, mut context, mut router) = frontend_rst_fixture(4);
23077        for n in 0..64u32 {
23078            let stream_id = 2 * n + 1;
23079            open_routed_frontend_stream(&mut connection, &mut context, stream_id, true);
23080            assert!(
23081                !client_cancels(&mut connection, &mut context, &mut router, stream_id),
23082                "reset {} of a stream whose response started must not send GOAWAY",
23083                n + 1
23084            );
23085        }
23086    }
23087
23088    /// A frontend connection whose pre-response cap has the floor `floor`,
23089    /// every other threshold at its default (soft refusal at 50 %).
23090    fn frontend_pre_response_fixture(
23091        floor: u64,
23092    ) -> (
23093        Rc<RefCell<Pool>>,
23094        H2Shell<mio::net::TcpStream>,
23095        std::net::TcpStream,
23096        Context<TestListener>,
23097        Router,
23098    ) {
23099        let pool = Rc::new(RefCell::new(Pool::with_capacity(8, 512, 16_384)));
23100        let (mut connection, peer) = test_h2_connection(&pool, None);
23101        let config = H2FloodConfig::from_optional(
23102            None,
23103            None,
23104            None,
23105            None,
23106            None,
23107            None,
23108            None,
23109            None,
23110            Some(floor),
23111            None,
23112            None,
23113            None,
23114            None,
23115            None,
23116        );
23117        connection.core.flood_detector =
23118            h2_flood_detector::H2FloodDetector::new(config, connection.core.now);
23119        let context = test_context(&pool);
23120        let router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
23121        (pool, connection, peer, context, router)
23122    }
23123
23124    /// A client that ignores `REFUSED_STREAM` and resets the streams Sōzu
23125    /// refused under flood pressure still reaches the pre-response cap: a
23126    /// refused stream never got a response, so its reset is a pre-response
23127    /// one even though the stream was never registered. A reset of a stream
23128    /// that is simply gone keeps counting as response-started.
23129    ///
23130    /// TO SEE THIS RED: in `ConnectionH2::handle_rst_stream_frame`, delete
23131    /// the `flood_refused_streams` arm. The resets then count as
23132    /// response-started, the pre-response count stops at the soft threshold,
23133    /// and no GOAWAY is ever sent.
23134    #[test]
23135    fn resets_of_streams_refused_under_flood_count_as_pre_response() {
23136        const FLOOR: u64 = 10;
23137        let (_pool, mut connection, _peer, mut context, mut router) =
23138            frontend_pre_response_fixture(FLOOR);
23139        let refused = (FLOOR as u32 + 1) * 2;
23140        for n in 0..refused {
23141            let stream_id = 2 * n + 1;
23142            connection
23143                .core
23144                .stream_table
23145                .observe_peer_stream_id(stream_id);
23146            connection.core.note_flood_refused_stream(stream_id);
23147        }
23148        assert_eq!(
23149            connection.core.flood_refused_streams,
23150            Some((1, 2 * refused - 1)),
23151            "consecutive refusals form one run"
23152        );
23153        // Resets of ids past the run: gone, not refused, never pre-response,
23154        // and no reason to stop refusing.
23155        for n in 0..=FLOOR as u32 {
23156            let stream_id = 2 * (refused + n) + 1;
23157            assert!(
23158                !client_cancels(&mut connection, &mut context, &mut router, stream_id),
23159                "a reset of a stream outside the refused run must not count as pre-response"
23160            );
23161        }
23162        assert!(!connection.core.flood_refusal_ignored);
23163        let mut tripped_at = None;
23164        for n in 0..refused {
23165            if client_cancels(&mut connection, &mut context, &mut router, 2 * n + 1) {
23166                tripped_at = Some(u64::from(n) + 1);
23167                break;
23168            }
23169        }
23170        assert_eq!(
23171            tripped_at,
23172            Some(FLOOR + 1),
23173            "resets of refused streams must reach the pre-response cap just past its floor"
23174        );
23175        assert!(
23176            connection.core.flood_refusal_ignored,
23177            "a client that resets a refused stream gets no more refusals"
23178        );
23179    }
23180
23181    /// An accepted stream ends the refused run; the next refusal starts a new
23182    /// one, so a stream that was served never counts as refused.
23183    #[test]
23184    fn an_accepted_stream_ends_the_flood_refused_run() {
23185        let (_pool, mut connection, _peer, mut context, _router) =
23186            frontend_pre_response_fixture(10);
23187        connection.core.note_flood_refused_stream(1);
23188        connection.core.note_flood_refused_stream(3);
23189        assert_eq!(connection.core.flood_refused_streams, Some((1, 3)));
23190        connection
23191            .core
23192            .create_stream(5, &mut context)
23193            .expect("the pool must hand out stream buffers");
23194        connection.core.note_flood_refused_stream(7);
23195        assert_eq!(
23196            connection.core.flood_refused_streams,
23197            Some((7, 7)),
23198            "a refusal after an accepted stream starts a new run"
23199        );
23200    }
23201
23202    /// A frontend connection over a [`PacedSocket`] with streams 1 and 3
23203    /// open, a pre-response floor of 10 and a glitch limit of `glitch_limit`,
23204    /// its SETTINGS acknowledged, and `pre_response_resets` pre-response
23205    /// resets on record. Six or more put it in the soft state: past half the
23206    /// floor, more than the streams it got answered (none).
23207    fn soft_refusal_connection(
23208        glitch_limit: Option<u32>,
23209        pre_response_resets: u32,
23210    ) -> (
23211        Rc<RefCell<Pool>>,
23212        H2Shell<PacedSocket>,
23213        Context<TestListener>,
23214        Router,
23215        std::net::TcpStream,
23216    ) {
23217        let (pool, mut connection, context, router, peer) = two_requests_read(usize::MAX);
23218        connection.core.flood_detector = h2_flood_detector::H2FloodDetector::new(
23219            H2FloodConfig::from_optional(
23220                None,
23221                None,
23222                None,
23223                None,
23224                None,
23225                None,
23226                glitch_limit,
23227                None,
23228                Some(10),
23229                None,
23230                None,
23231                None,
23232                None,
23233                None,
23234            ),
23235            connection.core.now,
23236        );
23237        connection.core.settings_sent_at = None;
23238        for _ in 0..pre_response_resets {
23239            assert!(
23240                connection
23241                    .core
23242                    .flood_detector
23243                    .record_rst_received(false, false, connection.core.now)
23244                    .is_none(),
23245                "premise: the resets on record stay under the cap"
23246            );
23247        }
23248        (pool, connection, context, router, peer)
23249    }
23250
23251    /// A complete `GET /` HEADERS frame on `stream_id`, from the static
23252    /// table and literals without indexing, so it decodes whatever the
23253    /// peer's dynamic table holds.
23254    fn static_get_headers(stream_id: StreamId) -> Vec<u8> {
23255        let mut block = vec![0x82, 0x84, 0x87, 0x01, 11];
23256        block.extend_from_slice(b"example.com");
23257        orphan_frame(
23258            1,
23259            parser::FLAG_END_STREAM | parser::FLAG_END_HEADERS,
23260            stream_id,
23261            block.len() as u32,
23262            &block,
23263        )
23264    }
23265
23266    /// Feed `inbound` to `connection`, flush what it answers, and return
23267    /// every frame on the wire so far.
23268    fn frames_after(
23269        connection: &mut H2Shell<PacedSocket>,
23270        context: &mut Context<TestListener>,
23271        router: &mut Router,
23272        inbound: &[u8],
23273    ) -> Vec<PeerFrame> {
23274        connection.socket.inbound.extend(inbound);
23275        // A pass may stop after a frame that queued output, so drive more
23276        // passes than there are frames.
23277        for _ in 0..256 {
23278            connection.core.readiness.event.insert(Ready::READABLE);
23279            if connection.core.readiness.filter_interest().is_readable() {
23280                connection.readable(context, EndpointClient(router));
23281            }
23282            connection.core.readiness.event.insert(Ready::WRITABLE);
23283            if connection.core.readiness.filter_interest().is_writable() {
23284                connection.writable(context, EndpointClient(router));
23285            }
23286        }
23287        peer_frames(&connection.socket.wire).expect("whole frames on the wire")
23288    }
23289
23290    /// The error code of the first frame of type `kind` (3 RST_STREAM, 7
23291    /// GOAWAY) on `stream_id` (0 for GOAWAY) in `frames`.
23292    fn error_code_of(frames: &[PeerFrame], kind: u8, stream_id: StreamId) -> Option<u32> {
23293        let at = if kind == 7 { 4 } else { 0 };
23294        frames
23295            .iter()
23296            .find(|(k, _, sid, _)| *k == kind && *sid == stream_id)
23297            .map(|(_, _, _, payload)| {
23298                u32::from_be_bytes(payload[at..at + 4].try_into().expect("an error code"))
23299            })
23300    }
23301
23302    /// RFC 9113 §5.1.1: a new stream's id must be greater than every id the
23303    /// client already used, refused ones included. After stream 65 is refused
23304    /// under flood pressure, HEADERS on 55 opens no stream: it is a
23305    /// connection error of type PROTOCOL_ERROR, and the refused run stays
23306    /// `(65, 65)`.
23307    ///
23308    /// TO SEE THIS RED: in `ConnectionH2::handle_header_state`, compare a new
23309    /// client stream's id with `last_stream_id` only, as before the fix. Stream
23310    /// 55 is then taken for a new stream and refused too, and the debug build
23311    /// panics with `the refused run ends at the stream just refused`.
23312    #[test]
23313    fn headers_below_a_refused_stream_id_is_a_protocol_error() {
23314        let (_pool, mut connection, mut context, mut router, _peer) =
23315            soft_refusal_connection(None, 6);
23316        let mut wire = static_get_headers(65);
23317        wire.extend(static_get_headers(55));
23318        let frames = frames_after(&mut connection, &mut context, &mut router, &wire);
23319        assert_eq!(
23320            error_code_of(&frames, 3, 65),
23321            Some(H2Error::RefusedStream as u32),
23322            "premise: stream 65 is refused under flood pressure, got {frames:?}"
23323        );
23324        assert_eq!(
23325            error_code_of(&frames, 7, 0),
23326            Some(H2Error::ProtocolError as u32),
23327            "HEADERS on an id below a refused one must end the connection with \
23328             GOAWAY(PROTOCOL_ERROR), got {frames:?}"
23329        );
23330        assert_eq!(
23331            error_code_of(&frames, 3, 55),
23332            None,
23333            "stream 55 must not be refused as a new stream"
23334        );
23335        assert_eq!(connection.core.flood_refused_streams, Some((65, 65)));
23336    }
23337
23338    /// Soft refusals count toward the glitch budget once the client
23339    /// acknowledged Sōzu's SETTINGS, like the draining and
23340    /// SETTINGS_MAX_CONCURRENT_STREAMS refusals: a client that keeps opening
23341    /// streams it is refused, without resetting any, ends with
23342    /// GOAWAY(ENHANCE_YOUR_CALM).
23343    ///
23344    /// TO SEE THIS RED: in `ConnectionH2::handle_header_state`, drop the
23345    /// `record_glitch` call of the soft-refusal branch. Every stream is then
23346    /// refused and no GOAWAY ever comes.
23347    #[test]
23348    fn soft_refusals_count_toward_the_glitch_budget() {
23349        const GLITCH_LIMIT: u32 = 20;
23350        let (_pool, mut connection, mut context, mut router, _peer) =
23351            soft_refusal_connection(Some(GLITCH_LIMIT), 6);
23352        let mut wire = Vec::new();
23353        for n in 0..2 * GLITCH_LIMIT {
23354            wire.extend(static_get_headers(5 + 2 * n));
23355        }
23356        let frames = frames_after(&mut connection, &mut context, &mut router, &wire);
23357        assert_eq!(
23358            error_code_of(&frames, 3, 5),
23359            Some(H2Error::RefusedStream as u32),
23360            "premise: the first new stream is refused under flood pressure"
23361        );
23362        assert_eq!(
23363            error_code_of(&frames, 7, 0),
23364            Some(H2Error::EnhanceYourCalm as u32),
23365            "a client that keeps opening refused streams must exhaust the glitch \
23366             budget, got {} RST_STREAM frames and no GOAWAY",
23367            frames.iter().filter(|(kind, ..)| *kind == 3).count()
23368        );
23369        assert!(
23370            frames.iter().filter(|(kind, ..)| *kind == 3).count() <= GLITCH_LIMIT as usize + 1,
23371            "the glitch budget bounds the refusals"
23372        );
23373    }
23374
23375    /// A RST_STREAM the client sends on the wire for a stream refused under
23376    /// flood pressure goes through the closed-stream branch of
23377    /// `ConnectionH2::handle_header_state`, which counts one glitch, and then
23378    /// reaches `ConnectionH2::handle_rst_stream_frame`: refusals stop for the
23379    /// connection, so its next stream is opened, and the reset counts as a
23380    /// pre-response one, so with ten pre-response resets on record it
23381    /// trips the cap's floor of 10.
23382    ///
23383    /// TO SEE THIS RED: in `ConnectionH2::handle_rst_stream_frame`, delete
23384    /// the `flood_refused_streams` arm: `a client that resets a refused
23385    /// stream gets no more refusals` fails, stream 7 is refused, and the
23386    /// reset counts as response-started, so no GOAWAY comes.
23387    #[test]
23388    fn a_client_reset_of_a_refused_stream_on_the_wire_ends_the_refusals() {
23389        let rst = |stream_id: StreamId| {
23390            orphan_frame(3, 0, stream_id, 4, &(H2Error::Cancel as u32).to_be_bytes())
23391        };
23392
23393        let (_pool, mut connection, mut context, mut router, _peer) =
23394            soft_refusal_connection(None, 6);
23395        let frames = frames_after(
23396            &mut connection,
23397            &mut context,
23398            &mut router,
23399            &static_get_headers(5),
23400        );
23401        assert_eq!(
23402            error_code_of(&frames, 3, 5),
23403            Some(H2Error::RefusedStream as u32),
23404            "premise: stream 5 is refused under flood pressure"
23405        );
23406        let glitches = connection.core.flood_detector.glitch_count();
23407        frames_after(&mut connection, &mut context, &mut router, &rst(5));
23408        assert_eq!(
23409            connection.core.flood_detector.glitch_count(),
23410            glitches + 1,
23411            "the reset of a refused stream costs one glitch"
23412        );
23413        assert!(
23414            connection.core.flood_refusal_ignored,
23415            "a client that resets a refused stream gets no more refusals"
23416        );
23417        let frames = frames_after(
23418            &mut connection,
23419            &mut context,
23420            &mut router,
23421            &static_get_headers(7),
23422        );
23423        assert_eq!(
23424            error_code_of(&frames, 3, 7),
23425            None,
23426            "no stream is refused after the client reset a refused one"
23427        );
23428        assert!(
23429            connection.core.stream_table.get(7).is_some(),
23430            "stream 7 is opened"
23431        );
23432
23433        // Ten pre-response resets on record: the reset of a refused stream is
23434        // the eleventh, past the floor.
23435        let (_pool, mut connection, mut context, mut router, _peer) =
23436            soft_refusal_connection(None, 10);
23437        let mut wire = static_get_headers(5);
23438        wire.extend(rst(5));
23439        let frames = frames_after(&mut connection, &mut context, &mut router, &wire);
23440        assert_eq!(
23441            error_code_of(&frames, 3, 5),
23442            Some(H2Error::RefusedStream as u32),
23443            "premise: stream 5 is refused under flood pressure"
23444        );
23445        assert_eq!(
23446            error_code_of(&frames, 7, 0),
23447            Some(H2Error::EnhanceYourCalm as u32),
23448            "the reset of a refused stream must count as pre-response, got {frames:?}"
23449        );
23450    }
23451
23452    /// HEADERS without END_STREAM, `len` bytes of DATA split in frames of at
23453    /// most 16 KiB, and a trailer section with END_STREAM on `stream_id`:
23454    /// what a client streaming an upload with request trailers sends before
23455    /// it reads anything back.
23456    fn upload_with_trailers(stream_id: StreamId, len: usize) -> Vec<u8> {
23457        let mut block = vec![0x83, 0x84, 0x87, 0x01, 11];
23458        block.extend_from_slice(b"example.com");
23459        let mut wire = orphan_frame(
23460            1,
23461            parser::FLAG_END_HEADERS,
23462            stream_id,
23463            block.len() as u32,
23464            &block,
23465        );
23466        wire.extend(refused_stream_data(stream_id, len, 0));
23467        // `x-t: 1`, a literal with incremental indexing and a new name: it
23468        // becomes the newest dynamic table entry, index 62 (RFC 7541 §6.2.1).
23469        let trailers = [0x40, 3, b'x', b'-', b't', 1, b'1'];
23470        wire.extend(orphan_frame(
23471            1,
23472            parser::FLAG_END_HEADERS | parser::FLAG_END_STREAM,
23473            stream_id,
23474            trailers.len() as u32,
23475            &trailers,
23476        ));
23477        wire
23478    }
23479
23480    /// `len` bytes of DATA on `stream_id` in frames of at most 16 KiB, the
23481    /// last one carrying `last_flags`.
23482    fn refused_stream_data(stream_id: StreamId, len: usize, last_flags: u8) -> Vec<u8> {
23483        let mut wire = Vec::new();
23484        let mut left = len;
23485        while left > 0 {
23486            let size = left.min(16_384);
23487            left -= size;
23488            let flags = if left == 0 { last_flags } else { 0 };
23489            wire.extend(orphan_frame(
23490                0,
23491                flags,
23492                stream_id,
23493                size as u32,
23494                &vec![b'x'; size],
23495            ));
23496        }
23497        wire
23498    }
23499
23500    /// RFC 9113 §5.1: the frames a client sent on a stream before it read
23501    /// Sōzu's `REFUSED_STREAM` are ignored, like those on a stream Sōzu
23502    /// reset. A refused upload carrying request trailers costs the client
23503    /// that stream only: no `RST_STREAM(STREAM_CLOSED)` for its DATA, no
23504    /// GOAWAY for its trailers, and the streams already open, and the
23505    /// connection, carry on.
23506    ///
23507    /// TO SEE THIS RED: in `ConnectionH2::handle_header_state`, drop the
23508    /// `remember_refused` call of the soft-refusal branch. The DATA
23509    /// frame then takes the closed-stream branch, and the trailers end the
23510    /// connection with GOAWAY(PROTOCOL_ERROR).
23511    #[test]
23512    fn trailers_on_a_refused_stream_are_ignored() {
23513        let (_pool, mut connection, mut context, mut router, _peer) =
23514            soft_refusal_connection(None, 6);
23515        let glitches = connection.core.flood_detector.glitch_count();
23516        let frames = frames_after(
23517            &mut connection,
23518            &mut context,
23519            &mut router,
23520            &upload_with_trailers(5, 1000),
23521        );
23522        assert_eq!(
23523            error_code_of(&frames, 3, 5),
23524            Some(H2Error::RefusedStream as u32),
23525            "premise: stream 5 is refused under flood pressure, got {frames:?}"
23526        );
23527        assert_eq!(
23528            frames
23529                .iter()
23530                .filter(|(kind, _, sid, _)| *kind == 3 && *sid == 5)
23531                .count(),
23532            1,
23533            "the refused stream gets its REFUSED_STREAM and nothing more, got {frames:?}"
23534        );
23535        assert_eq!(
23536            error_code_of(&frames, 7, 0),
23537            None,
23538            "frames on a refused stream must not end the connection, got {frames:?}"
23539        );
23540        assert_eq!(
23541            connection.core.flood_detector.glitch_count(),
23542            glitches + 1,
23543            "only the refusal itself costs a glitch"
23544        );
23545        assert!(
23546            connection.core.stream_table.get(1).is_some()
23547                && connection.core.stream_table.get(3).is_some(),
23548            "the streams already open survive"
23549        );
23550        assert!(
23551            !matches!(connection.core.state, H2State::GoAway | H2State::Error),
23552            "the connection survives, got {:?}",
23553            connection.core.state
23554        );
23555        // The ignored trailer block was still decoded (RFC 9113 §4.3): the
23556        // entry it indexed is the newest in the connection's dynamic table.
23557        let mut decoded = Vec::new();
23558        let status = connection
23559            .core
23560            .hpack
23561            .decoder_mut()
23562            .decode_with_cb(&[0x80 | 62], |k, v| {
23563                decoded.push((k.into_owned(), v.into_owned()));
23564            });
23565        assert!(status.is_ok(), "index 62 must resolve, got {status:?}");
23566        assert_eq!(
23567            decoded,
23568            vec![(b"x-t".to_vec(), b"1".to_vec())],
23569            "the ignored trailers must keep the HPACK decoder in step with the peer"
23570        );
23571        // The connection reads on: a later PING is answered.
23572        let frames = frames_after(&mut connection, &mut context, &mut router, &orphan_ping());
23573        assert!(
23574            frames.iter().any(|(kind, flags, _, payload)| *kind == 6
23575                && *flags & 1 == 1
23576                && payload == b"pingpong"),
23577            "the connection must keep reading after the ignored frames, got {frames:?}"
23578        );
23579    }
23580
23581    /// RFC 9113 §5.1 and §6.9: DATA a client sent on a refused stream within
23582    /// the stream's initial window — a browser upload filling it in 16 KiB
23583    /// frames — is ignored without a glitch or a `RST_STREAM(STREAM_CLOSED)`
23584    /// per frame, and still counts toward the connection's receive window,
23585    /// which Sōzu credits back.
23586    ///
23587    /// TO SEE THIS RED: in `ConnectionH2::handle_header_state`, drop the
23588    /// `remember_refused` call of the soft-refusal branch. Each of
23589    /// the four DATA frames then costs a glitch and a RST_STREAM.
23590    #[test]
23591    fn data_on_a_refused_stream_is_ignored_and_credited() {
23592        let (_pool, mut connection, mut context, mut router, _peer) =
23593            soft_refusal_connection(None, 6);
23594        // Half of this is the threshold at which a stream-0 WINDOW_UPDATE
23595        // credits the received bytes back.
23596        connection.core.connection_config.initial_connection_window = 65_535;
23597        let mut block = vec![0x83, 0x84, 0x87, 0x01, 11];
23598        block.extend_from_slice(b"example.com");
23599        let mut wire = orphan_frame(1, parser::FLAG_END_HEADERS, 5, block.len() as u32, &block);
23600        let window = connection.core.local_settings.settings_initial_window_size as usize;
23601        wire.extend(refused_stream_data(5, window, parser::FLAG_END_STREAM));
23602        let glitches = connection.core.flood_detector.glitch_count();
23603        let frames = frames_after(&mut connection, &mut context, &mut router, &wire);
23604        assert_eq!(
23605            error_code_of(&frames, 3, 5),
23606            Some(H2Error::RefusedStream as u32),
23607            "premise: stream 5 is refused under flood pressure, got {frames:?}"
23608        );
23609        assert_eq!(
23610            frames
23611                .iter()
23612                .filter(|(kind, _, sid, _)| *kind == 3 && *sid == 5)
23613                .count(),
23614            1,
23615            "DATA on a refused stream must not be answered with RST_STREAM, got {frames:?}"
23616        );
23617        assert_eq!(
23618            connection.core.flood_detector.glitch_count(),
23619            glitches + 1,
23620            "DATA within the stream's window on a refused stream costs no glitch"
23621        );
23622        let credited: u32 = frames
23623            .iter()
23624            .filter(|(kind, _, sid, _)| *kind == 8 && *sid == 0)
23625            .map(|(_, _, _, payload)| {
23626                u32::from_be_bytes(payload[..4].try_into().expect("an increment")) & 0x7fff_ffff
23627            })
23628            .sum();
23629        assert_eq!(
23630            credited as usize, window,
23631            "the ignored DATA must be credited to the connection window exactly once"
23632        );
23633        assert_eq!(
23634            error_code_of(&frames, 7, 0),
23635            None,
23636            "no GOAWAY, got {frames:?}"
23637        );
23638    }
23639
23640    /// A drain refuses new streams; a client that sent a whole upload with
23641    /// request trailers on one of them before it read the GOAWAY sees its
23642    /// stream refused, and the drain still ends with GOAWAY(NO_ERROR).
23643    ///
23644    /// TO SEE THIS RED: in `ConnectionH2::refuse_stream_and_discard`, drop
23645    /// the `remember_refused` call. The trailers then end the
23646    /// connection with GOAWAY(PROTOCOL_ERROR) before the drain completes.
23647    #[test]
23648    fn a_drain_with_a_refused_upload_carrying_trailers_ends_with_no_error() {
23649        let (_pool, mut connection, mut context, mut router, _peer) = two_requests_read(usize::MAX);
23650        connection.core.drain.__test_set_draining();
23651        let frames = frames_after(
23652            &mut connection,
23653            &mut context,
23654            &mut router,
23655            &upload_with_trailers(5, 1000),
23656        );
23657        assert_eq!(
23658            error_code_of(&frames, 3, 5),
23659            Some(H2Error::RefusedStream as u32),
23660            "premise: the drain refuses stream 5, got {frames:?}"
23661        );
23662        let now = connection.core.now;
23663        let _ = connection.core.graceful_goaway(now);
23664        let frames = frames_after(&mut connection, &mut context, &mut router, &[]);
23665        assert_eq!(
23666            error_code_of(&frames, 7, 0),
23667            Some(H2Error::NoError as u32),
23668            "the drain must end with GOAWAY(NO_ERROR), got {frames:?}"
23669        );
23670    }
23671
23672    /// A mix of cancels: one stream in ten reset before its response,
23673    /// seventeen in twenty after it, the rest completed. The pre-response
23674    /// resets are the pre-response cap's business; the received-RST lifetime
23675    /// cap compares only the other resets with the answered streams, and must
23676    /// not trip.
23677    ///
23678    /// TO SEE THIS RED: in `H2FloodDetector::record_rst_lifetime`, compare
23679    /// `total_rst_received_lifetime` instead of the resets outside the
23680    /// pre-response count with `streams_opened`. The pre-response resets then
23681    /// push the count past the answered streams and the cap sends GOAWAY.
23682    #[test]
23683    fn pre_response_resets_do_not_count_toward_the_lifetime_cap() {
23684        let (_pool, mut connection, _peer, mut context, mut router) = frontend_rst_fixture(4);
23685        let mut stream_id = 1;
23686        for block in 0..5 {
23687            for slot in 0..20 {
23688                if slot == 19 {
23689                    // A stream that completed: counted where the recycle path
23690                    // counts it (`try_recycle_server_stream`).
23691                    connection.core.flood_detector.record_stream_opened();
23692                    continue;
23693                }
23694                let response_started = slot >= 2;
23695                open_routed_frontend_stream(
23696                    &mut connection,
23697                    &mut context,
23698                    stream_id,
23699                    response_started,
23700                );
23701                assert!(
23702                    !client_cancels(&mut connection, &mut context, &mut router, stream_id),
23703                    "block {block}, slot {slot}: a 10 % pre-response / 85 % \
23704                     post-response / 5 % clean mix must not send GOAWAY"
23705                );
23706                stream_id += 2;
23707            }
23708        }
23709    }
23710
23711    /// The received-RST lifetime cap still bounds resets of streams that are
23712    /// already gone: here every answered stream is reset twice, so these
23713    /// resets outnumber the answered streams as soon as the floor is passed.
23714    /// Guard against neutering the cap.
23715    #[test]
23716    fn resets_of_streams_already_gone_trip_the_lifetime_cap() {
23717        let (_pool, mut connection, _peer, mut context, mut router) = frontend_rst_fixture(4);
23718        let mut tripped_after = None;
23719        for n in 0..16u32 {
23720            let stream_id = 2 * n + 1;
23721            open_routed_frontend_stream(&mut connection, &mut context, stream_id, true);
23722            if client_cancels(&mut connection, &mut context, &mut router, stream_id)
23723                || client_cancels(&mut connection, &mut context, &mut router, stream_id)
23724            {
23725                tripped_after = Some(n + 1);
23726                break;
23727            }
23728        }
23729        assert_eq!(
23730            tripped_after,
23731            Some(3),
23732            "the reset that takes the count past the floor of 4 while the \
23733             resets outnumber the answered streams must send GOAWAY"
23734        );
23735    }
23736
23737    /// A stream Sōzu answers 502/503/504 without selecting a backend — no
23738    /// backend in the cluster, the session or buffer limit hit before a
23739    /// backend was picked, a routing error — never cost a backend anything,
23740    /// and must not count toward the RST caps' denominator.
23741    ///
23742    /// TO SEE THIS RED: add `|| matches!(stream.context.status,
23743    /// Some(502..=504))` back to `routed_to_a_backend`.
23744    #[test]
23745    fn a_5xx_answered_without_a_backend_is_not_routed() {
23746        let pool = make_pool_for_invariant_16();
23747        let (mut connection, _peer) = test_h2_connection(&pool, None);
23748        let mut context = test_context(&pool);
23749        let answers = context.listener.borrow().get_answers().clone();
23750        for code in [502, 503, 504] {
23751            let gid = context
23752                .create_stream(Ulid::generate(), 65_535)
23753                .expect("the test pool must hand out stream buffers");
23754            crate::protocol::mux::answers::set_default_answer(
23755                &mut context.streams[gid],
23756                &mut connection.core.readiness,
23757                code,
23758                &answers.borrow(),
23759            );
23760            let stream = &context.streams[gid];
23761            assert_eq!(stream.context.status, Some(code), "premise");
23762            assert!(
23763                !routed_to_a_backend(stream),
23764                "a {code} answered before any backend was selected must not count"
23765            );
23766            context.streams[gid].state = StreamState::Recycle;
23767        }
23768    }
23769
23770    /// A stream answered 502/503/504 after a backend was selected for it —
23771    /// the backend refused the connection, timed out, or reset — keeps
23772    /// counting, so a backend outage does not turn the client's ordinary
23773    /// cancels into a Rapid Reset verdict.
23774    ///
23775    /// TO SEE THIS RED: drop `|| stream.context.backend_id.is_some()` from
23776    /// `routed_to_a_backend`.
23777    ///
23778    /// Past the first failures the outage no longer selects a backend at all:
23779    /// every backend is backing off and the 503 is answered with
23780    /// `backends_unavailable` set, which counts the same way.
23781    ///
23782    /// TO SEE THIS RED for the second half: drop
23783    /// `|| stream.context.backends_unavailable` from `routed_to_a_backend`;
23784    /// `test_h2_cancels_during_a_backend_outage_keep_the_connection` then
23785    /// fails end to end too.
23786    #[test]
23787    fn a_5xx_answered_during_a_backend_outage_is_routed() {
23788        let pool = make_pool_for_invariant_16();
23789        let (mut connection, _peer) = test_h2_connection(&pool, None);
23790        let mut context = test_context(&pool);
23791        let answers = context.listener.borrow().get_answers().clone();
23792        for code in [502, 503, 504] {
23793            let gid = context
23794                .create_stream(Ulid::generate(), 65_535)
23795                .expect("the test pool must hand out stream buffers");
23796            context.streams[gid].context.backend_id = Some(Rc::from("down-backend"));
23797            crate::protocol::mux::answers::set_default_answer(
23798                &mut context.streams[gid],
23799                &mut connection.core.readiness,
23800                code,
23801                &answers.borrow(),
23802            );
23803            let stream = &context.streams[gid];
23804            assert_eq!(stream.context.status, Some(code), "premise");
23805            assert!(
23806                routed_to_a_backend(stream),
23807                "a {code} answered for a selected backend must count"
23808            );
23809            context.streams[gid].state = StreamState::Recycle;
23810        }
23811        let gid = context
23812            .create_stream(Ulid::generate(), 65_535)
23813            .expect("the test pool must hand out stream buffers");
23814        context.streams[gid].context.backends_unavailable = true;
23815        crate::protocol::mux::answers::set_default_answer(
23816            &mut context.streams[gid],
23817            &mut connection.core.readiness,
23818            503,
23819            &answers.borrow(),
23820        );
23821        assert!(
23822            routed_to_a_backend(&context.streams[gid]),
23823            "a 503 for a cluster whose backends are all failing must count"
23824        );
23825    }
23826
23827    /// An inbound `GOAWAY` retires every stream above `last_stream_id`
23828    /// without `Connection::end_stream`, so it owes each of their releases.
23829    #[test]
23830    fn invariant_14_an_inbound_goaway_releases_every_retired_charge() {
23831        let pool = make_pool_for_invariant_16();
23832        let mut context = test_context(&pool);
23833        let mut fixture = ledger_fixture(&pool);
23834        let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
23835
23836        let _gids = open_streams(&mut fixture, &mut context, 3);
23837        let Connection::H2(shell) = &mut fixture.connection else {
23838            unreachable!("the fixture was built as H2");
23839        };
23840        // `last_stream_id = 0` retires all three: the peer says it processed
23841        // none of them.
23842        shell.core.handle_goaway_frame(
23843            parser::GoAway {
23844                last_stream_id: 0,
23845                error_code: H2Error::NoError as u32,
23846                // Empty debug data: `Slice` is an offset/length pair into
23847                // a buffer this scenario does not carry.
23848                additional_debug_data: kawa::repr::Slice::new(b"", b""),
23849            },
23850            &mut context,
23851            EndpointClient(&mut router),
23852        );
23853
23854        assert_eq!(
23855            reconcile(&context.backend_deltas),
23856            (0, 0),
23857            "every retired stream must release the charge it took"
23858        );
23859        assert_eq!(
23860            settle(&mut fixture, &mut context),
23861            0,
23862            "a GOAWAY that retires every stream must leave no charge behind"
23863        );
23864    }
23865
23866    /// An H1 backend taken back out of the keep-alive pool must be charged
23867    /// for the request it is being reused for.
23868    ///
23869    /// This one records a BEHAVIOUR CHANGE, not just a refactor. Before
23870    /// #1340's Question 12 the charge was taken *before* the inner
23871    /// `start_stream`, under a `BackendStatus::Connected` guard — and an H1
23872    /// connection coming out of the pool is `BackendStatus::KeepAlive` at
23873    /// that moment, because `ConnectionH1::start_stream` is what flips it to
23874    /// `Connected`. So the reuse took no charge, while the matching
23875    /// `Connection::end_stream` released one (it runs before
23876    /// `ConnectionH1::end_stream` flips the status back, so its own
23877    /// `Connected` guard passes). Every keep-alive reuse cycle therefore net
23878    /// `-1`d the backend, `saturating_sub` floored it at zero, and a reused
23879    /// H1 connection reported no in-flight request while it served one —
23880    /// exactly the monotonic drift invariant 14 names, biasing least-loaded
23881    /// and PeakEWMA balancing toward backends that are already busy.
23882    ///
23883    /// Recording the charge after the inner call fixes it, because the flip
23884    /// has happened by then. Taking it afterwards is also what removes the
23885    /// rollback, so the two properties are one edit.
23886    #[test]
23887    fn invariant_14_an_h1_keep_alive_reuse_is_charged() {
23888        let pool = make_pool_for_invariant_16();
23889        let mut context = test_context(&pool);
23890        let (socket, _peer) = connected_socket();
23891        let address: std::net::SocketAddr =
23892            "127.0.0.1:2".parse().expect("backend address must parse");
23893        let backend = Rc::new(RefCell::new(crate::backends::Backend::new(
23894            "h1-keepalive-backend",
23895            address,
23896            None,
23897            None,
23898            None,
23899        )));
23900        let mut registry = crate::protocol::mux::BackendRegistry::default();
23901        let backend_id = registry.id_for(&backend);
23902        let session_ulid = Ulid::generate();
23903        let mut connection = Connection::new_h1_client(
23904            session_ulid,
23905            SessionTcpStream::new(socket, session_ulid, Some(address)),
23906            "h1-keepalive-cluster".into(),
23907            backend_id,
23908            Duration::from_secs(30),
23909        );
23910        // Park it in the pool, the state `Router::plan_connect`'s H1 reuse arm
23911        // picks a backend up from.
23912        if let Position::Client(_, _, status) = connection.position_mut() {
23913            *status = BackendStatus::KeepAlive;
23914        }
23915
23916        let gid = context
23917            .create_stream(Ulid::generate(), 65_535)
23918            .expect("the test pool must hand out stream buffers");
23919        assert!(
23920            connection.start_stream(gid, &mut context),
23921            "premise: a keep-alive H1 backend must accept the reused stream"
23922        );
23923        assert_eq!(
23924            reconcile(&context.backend_deltas),
23925            (1, 0),
23926            "a keep-alive reuse must charge the request it is being reused for"
23927        );
23928
23929        connection.end_stream(gid, &mut context);
23930        assert_eq!(
23931            reconcile(&context.backend_deltas),
23932            (0, 0),
23933            "and the charge must be released when that request ends"
23934        );
23935        let backends = RefCell::new(crate::backends::BackendMap::with_seed(0));
23936        for delta in std::mem::take(&mut context.backend_deltas) {
23937            registry.apply(delta, &backends);
23938        }
23939        assert_eq!(
23940            backend.borrow().active_requests,
23941            0,
23942            "a full reuse cycle must leave active_requests where it found it"
23943        );
23944    }
23945
23946    /// `cancel_timed_out_streams` reaps an idle stream on the same terms:
23947    /// removed outside `Connection::end_stream`, so it releases its own
23948    /// charge.
23949    #[test]
23950    fn invariant_14_a_timed_out_stream_releases_its_charge() {
23951        let pool = make_pool_for_invariant_16();
23952        let mut context = test_context(&pool);
23953        let mut fixture = ledger_fixture(&pool);
23954        let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
23955
23956        let _gids = open_streams(&mut fixture, &mut context, 1);
23957        // Push the mux's clock far past the stream idle timeout the fixture
23958        // configured, so the reaper finds the stream expired.
23959        context.now += Duration::from_secs(120);
23960        let Connection::H2(shell) = &mut fixture.connection else {
23961            unreachable!("the fixture was built as H2");
23962        };
23963        let mut endpoint = EndpointClient(&mut router);
23964        shell
23965            .core
23966            .cancel_timed_out_streams(&mut context, &mut endpoint);
23967
23968        assert_eq!(
23969            reconcile(&context.backend_deltas),
23970            (0, 0),
23971            "a reaped stream must release the charge it took"
23972        );
23973        assert_eq!(
23974            settle(&mut fixture, &mut context),
23975            0,
23976            "reaping the only stream must leave no charge behind"
23977        );
23978    }
23979
23980    // ── `backend_header_time`, the H2 half (sozu-proxy/sozu#426) ─────────
23981    //
23982    // DO NOT READ THIS TEST AS COVERING H1. It drives
23983    // `ConnectionH2::handle_headers_frame` only. The H1 twin lives in
23984    // `ConnectionH1::readable` (`lib/src/protocol/mux/h1.rs`) and is pinned by
23985    // its own test there.
23986
23987    /// Stage a response field block in the connection's stream-0 storage and
23988    /// hand back the `Headers` frame that names it, the way
23989    /// `ConnectionH2::handle_read` hands one to
23990    /// `ConnectionH2::handle_headers_frame`: the fragment is a `(start, len)`
23991    /// window over `ConnectionH2::zero`'s buffer, not an owned slice.
23992    ///
23993    /// `0x88` is one HPACK indexed-header-field octet naming static-table
23994    /// entry 8, `:status: 200` (RFC 7541 Appendix A) — a complete, valid
23995    /// response field block in a single byte, so this needs no encoder.
23996    fn stage_response_headers(core: &mut ConnectionH2, stream_id: u32) -> parser::Headers {
23997        core.zero.storage.space()[..1].copy_from_slice(&[0x88]);
23998        core.zero.storage.fill(1);
23999        let buffer = core.zero.storage.buffer();
24000        parser::Headers {
24001            stream_id,
24002            priority: None,
24003            header_block_fragment: kawa::repr::Slice::new(buffer, &buffer[..1]),
24004            end_stream: false,
24005            end_headers: true,
24006        }
24007    }
24008
24009    /// The H2 twin of
24010    /// `the_h1_backend_header_instant_lands_on_the_headers_not_the_response_end`:
24011    /// a backend response HEADERS frame arms the time-to-first-header-byte
24012    /// instant, and it arms it strictly before the response ends.
24013    ///
24014    /// `end_stream: false` is what makes it discriminating — the response body
24015    /// is still outstanding, so `backend_stop` (which every one of
24016    /// `ConnectionH2`'s four sites owns) must still be absent while the header
24017    /// instant already exists. An instant placed at any of those sites cannot
24018    /// satisfy that.
24019    ///
24020    /// The ordering is asserted on `Duration`s rather than on the millisecond
24021    /// values the histogram stores, for the same reason as its H1 twin: at
24022    /// millisecond resolution a fast test rounds both to 0 and the relation
24023    /// holds however wrong the placement is.
24024    ///
24025    /// To SEE THIS RED: move `stream.metrics.backend_headers_received();` out
24026    /// of the `was_initial && self.position.is_client()` branch of
24027    /// `ConnectionH2::handle_headers_frame` and put it immediately after the
24028    /// `stream.metrics.backend_stop();` that opens
24029    /// `ConnectionH2::complete_server_stream` — the nominal `H2::Complete`
24030    /// response-end site. The frame under test carries no END_STREAM, so
24031    /// nothing arms the instant and the test reports no header time at all.
24032    #[test]
24033    fn the_h2_backend_header_instant_lands_on_the_headers_not_the_response_end() {
24034        let pool = make_pool_for_invariant_16();
24035        let mut context = test_context(&pool);
24036        let mut fixture = ledger_fixture(&pool);
24037        let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
24038
24039        let gids = open_streams(&mut fixture, &mut context, 1);
24040        let gid = gids[0];
24041        let metrics = &mut context.streams[gid].metrics;
24042        metrics.backend_id = Some("ledger-backend".into());
24043        metrics.backend_start();
24044        metrics.backend_connected();
24045
24046        // Premise: nothing has been received, so neither instant can be stale.
24047        assert_eq!(context.streams[gid].metrics.backend_header_time(), None);
24048        assert_eq!(context.streams[gid].metrics.backend_stop, None);
24049
24050        let Connection::H2(shell) = &mut fixture.connection else {
24051            unreachable!("the fixture was built as H2");
24052        };
24053        let stream_id = *shell
24054            .core
24055            .stream_table
24056            .streams()
24057            .keys()
24058            .next()
24059            .expect("the scenario opened one stream");
24060        assert!(
24061            context.streams[gid].back.is_initial(),
24062            "premise: the backend read side must still be in its initial \
24063             parsing phase, or `was_initial` would not fire"
24064        );
24065
24066        let headers = stage_response_headers(&mut shell.core, stream_id);
24067        shell
24068            .core
24069            .handle_headers_frame(headers, &mut context, EndpointClient(&mut router));
24070
24071        let after_headers = context.streams[gid]
24072            .metrics
24073            .backend_headers_received
24074            .expect("a backend response HEADERS frame must arm the backend header instant");
24075        assert_eq!(
24076            context.streams[gid].metrics.backend_stop, None,
24077            "the frame carried no END_STREAM, so the response-end marker must \
24078             still be absent"
24079        );
24080
24081        // What `ConnectionH2::mark_end_of_stream` does once the response ends.
24082        context.streams[gid].metrics.backend_stop();
24083
24084        let metrics = &context.streams[gid].metrics;
24085        assert_eq!(
24086            metrics.backend_headers_received,
24087            Some(after_headers),
24088            "the response end must not move the header instant"
24089        );
24090        let header_time = metrics
24091            .backend_header_time()
24092            .expect("a completed H2 response must report a backend header time");
24093        let response_time = metrics
24094            .backend_response_time()
24095            .expect("a completed H2 response must report a backend response time");
24096        assert!(
24097            header_time <= response_time,
24098            "the first response-header byte arrives before the last response \
24099             byte: header_time {header_time:?} must not exceed response_time \
24100             {response_time:?}"
24101        );
24102    }
24103
24104    /// The server preface — SETTINGS, the connection-window enlargement and
24105    /// the ACK of the client's SETTINGS — leaves in ONE socket write.
24106    ///
24107    /// On `FrontRustls` every non-empty `socket_write` ends in its own
24108    /// `write_tls`, i.e. its own `writev(2)`. The enlargement used to be
24109    /// queued by `dispatch_writable_state`'s `(ServerSettings, Server)` arm,
24110    /// which runs after `drive_control_flush` already drained `zero`, so it
24111    /// cost a second write pass and a second syscall on every connection.
24112    ///
24113    /// TO SEE THIS RED: move the `gen_window_update` call out of the
24114    /// `(ClientSettings, Server)` readable arm back into a
24115    /// `queue_window_update(0, increment)` in the `(ServerSettings, Server)`
24116    /// writable arm. The write count becomes 2 and the first write carries 66
24117    /// bytes, not 79.
24118    #[test]
24119    fn server_preface_settings_window_update_and_ack_share_one_write() {
24120        use std::io::Read as _;
24121
24122        let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 8, 16384)));
24123        let (mut connection, mut peer) =
24124            connection_with_backpressure(&pool, 0, 0, H2State::ClientPreface);
24125        connection
24126            .core
24127            .readiness
24128            .interest
24129            .insert(Ready::READABLE | Ready::WRITABLE);
24130        let mut context = test_context(&pool);
24131        let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
24132
24133        // Client preface + SETTINGS(MAX_CONCURRENT_STREAMS = 100).
24134        let mut preface = serializer::H2_PRI.as_bytes().to_vec();
24135        preface.extend_from_slice(&[0, 0, 6, 4, 0, 0, 0, 0, 0]);
24136        preface.extend_from_slice(&parser::SETTINGS_MAX_CONCURRENT_STREAMS.to_be_bytes());
24137        preface.extend_from_slice(&100u32.to_be_bytes());
24138        let now = connection.core.now;
24139        liveness_read(
24140            &mut connection,
24141            &mut context,
24142            &mut router,
24143            &mut peer,
24144            now,
24145            &preface,
24146        );
24147        liveness_write(&mut connection, &mut context, &mut router);
24148
24149        assert!(
24150            matches!(connection.core.state, H2State::Header),
24151            "premise: the handshake must have reached the proxying state, got {:?}",
24152            connection.core.state
24153        );
24154        assert_eq!(
24155            connection.socket.writes, 1,
24156            "SETTINGS, WINDOW_UPDATE(0) and the SETTINGS ACK must leave in a \
24157             single socket write"
24158        );
24159
24160        let settings_len = parser::FRAME_HEADER_SIZE
24161            + (parser::SETTINGS_ENTRY_SIZE * parser::SETTINGS_COUNT) as usize;
24162        let expected = settings_len + 13 + serializer::SETTINGS_ACKNOWLEDGEMENT.len();
24163        let mut wire = vec![0u8; expected];
24164        peer.set_read_timeout(Some(Duration::from_secs(5)))
24165            .expect("loopback read timeout must be settable");
24166        peer.read_exact(&mut wire)
24167            .expect("the peer must receive the whole server preface");
24168        assert_eq!(
24169            wire[3], 4,
24170            "the first frame must be SETTINGS (RFC 9113 §3.4)"
24171        );
24172        assert_eq!(wire[4], 0, "the first SETTINGS must not be an ACK");
24173        let window_update = &wire[settings_len..settings_len + 13];
24174        assert_eq!(
24175            window_update[3], 8,
24176            "the second frame must be WINDOW_UPDATE"
24177        );
24178        assert_eq!(&window_update[5..9], &[0, 0, 0, 0], "on the connection");
24179        assert_eq!(
24180            u32::from_be_bytes(window_update[9..13].try_into().unwrap()),
24181            H2ConnectionConfig::default().initial_connection_window - DEFAULT_INITIAL_WINDOW_SIZE,
24182            "the enlargement must grant exactly the configured surplus"
24183        );
24184        assert_eq!(
24185            &wire[settings_len + 13..],
24186            &serializer::SETTINGS_ACKNOWLEDGEMENT,
24187            "the ACK of the client's SETTINGS must close the preface"
24188        );
24189    }
24190
24191    /// A backend that sends SETTINGS again, without granting any connection
24192    /// credit, must not get the one-shot connection-window enlargement again:
24193    /// each repeat would add `initial_connection_window - 65535` to its send
24194    /// window until it passed 2^31-1 (RFC 9113 §6.9.1).
24195    ///
24196    /// TO SEE THIS RED: in `ConnectionH2::handle_settings_frame`, gate the
24197    /// enlargement on `self.flow_control.window() <= DEFAULT_INITIAL_WINDOW_SIZE`
24198    /// (our send window) instead of `connection_window_enlarged`. The three
24199    /// SETTINGS then queue three grants, coalesced into one stream-0 frame
24200    /// of three times the surplus. Verified 2026-10-01 against `3d3f077b`.
24201    #[test]
24202    fn repeated_backend_settings_enlarge_the_connection_window_once() {
24203        let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
24204        let (mut connection, _peer) = test_h2_connection(&pool, None);
24205        let mut context = test_context(&pool);
24206        connection.core.position = Position::Client(
24207            "settings-cluster".into(),
24208            super::super::BackendId {
24209                slot: super::super::BackendSlot(0),
24210                backend_id: Rc::from("settings-backend"),
24211                address: "127.0.0.1:1".parse().expect("a literal socket address"),
24212                metric_incarnation: Default::default(),
24213            },
24214            BackendStatus::Connected,
24215        );
24216        for _ in 0..3 {
24217            let result = connection.core.handle_settings_frame(
24218                parser::Settings {
24219                    settings: vec![],
24220                    ack: false,
24221                },
24222                &mut context,
24223            );
24224            assert!(
24225                matches!(result, MuxResult::Continue),
24226                "an empty SETTINGS must be accepted"
24227            );
24228        }
24229
24230        let mut buf = vec![0u8; 13 * 8];
24231        let (written, frames) = connection
24232            .core
24233            .flow_control
24234            .drain_window_updates_into(&mut buf);
24235        let grants: Vec<(u32, u32)> = buf[..written]
24236            .as_chunks::<13>()
24237            .0
24238            .iter()
24239            .map(|frame| {
24240                (
24241                    u32::from_be_bytes(frame[5..9].try_into().unwrap()),
24242                    u32::from_be_bytes(frame[9..13].try_into().unwrap()),
24243                )
24244            })
24245            .collect();
24246        assert_eq!(frames, grants.len());
24247        assert_eq!(
24248            grants,
24249            vec![(
24250                0,
24251                H2ConnectionConfig::default().initial_connection_window
24252                    - DEFAULT_INITIAL_WINDOW_SIZE
24253            )],
24254            "three SETTINGS must enlarge the connection window exactly once"
24255        );
24256    }
24257}