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 the [`H2FloodDetector`] backing
5//! the CVE-2023-44487 / CVE-2024-27316 / CVE-2025-8671 mitigations. Stream
6//! storage lives in the sibling `Context<L>` (`mux/mod.rs`); this module is
7//! the canonical home for the edge-trigger discipline — paths that queue
8//! bytes for a later event-loop pass must arm writable / signal pending
9//! write (cf. `arm_writable()` at the deferred-control-frame sites and
10//! `lib/src/lib.rs:1006`-`1010`).
11
12use std::{
13    cmp::min,
14    collections::{HashMap, HashSet},
15    io::{IoSlice, Write as _},
16    time::{Duration, Instant},
17};
18
19/// Compile-time guard: `payload_len as usize` casts in the H2 parser assume at
20/// least 32-bit pointer width.  This prevents silent truncation on platforms
21/// with smaller pointers (e.g. 16-bit embedded targets).
22const _: () = assert!(
23    std::mem::size_of::<usize>() >= 4,
24    "sozu requires at least 32-bit pointers"
25);
26
27use rusty_ulid::Ulid;
28use sozu_command::{logging::ansi_palette, ready::Ready};
29
30use crate::metrics::names;
31use crate::{
32    L7ListenerHandler, ListenerHandler, Protocol, Readiness, SessionMetrics,
33    protocol::mux::{
34        BackendStatus, Context, DebugEvent, DebugHistory, Endpoint, GenericHttpStream,
35        GlobalStreamId, MuxResult, Position, Stream, StreamId, StreamState, converter,
36        forcefully_terminate_answer,
37        parser::{self, Frame, FrameHeader, FrameType, H2Error, Headers, WindowUpdate},
38        pkawa, remove_backend_stream, serializer, set_default_answer,
39        shared::{EndStreamAction, drain_tls_close_notify, end_stream_decision},
40        update_readiness_after_read, update_readiness_after_write,
41    },
42    socket::{SocketHandler, SocketResult, stats::socket_rtt},
43    timer::TimeoutContainer,
44};
45
46/// Protocol label + session descriptor used as a prefix on every
47/// [`ConnectionH2`] log line. Matches the RUSTLS log-context convention:
48/// `MUX-H2\tSession(...)\t >>>`. When colored output is enabled (via
49/// [`ansi_palette`]) the label is wrapped in bold bright-white ANSI (uniform
50/// across every protocol) and the session detail is rendered in light grey.
51///
52/// Fields included in the session block (chosen to surface the most common
53/// H2 troubleshooting axes — flow stall, leaked stream, draining state,
54/// peer-side gap, reset-flood exposure):
55/// - `peer` — peer address (or `None` if the socket is gone)
56/// - `position` — `Server` / `Client(...)` orientation
57/// - `state` — current [`H2State`]
58/// - `streams` — number of in-flight streams on this connection
59/// - `last_peer_id` — `highest_peer_stream_id` (gap to the peer's view)
60/// - `window` — connection-level send window (RFC 9113 §6.9)
61/// - `draining` — set after the first GOAWAY of a graceful shutdown
62/// - `total_rst_streams_emitted_lifetime` — MadeYouReset counter (CVE-2025-8671)
63/// - `total_rst_received_lifetime` — Rapid Reset counter (CVE-2023-44487)
64/// - `readiness` — connection-level mio readiness snapshot
65///
66/// Computed lazily on each callsite — the helper only materialises when the
67/// log level is enabled, so uncolored hot paths keep a single thread-local
68/// read (the colored check) and one `format!` allocation.
69macro_rules! log_context {
70    ($self:expr) => {{
71        let (open, reset, grey, gray, white) = ansi_palette();
72        format!(
73            "[{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 >>>",
74            open = open,
75            reset = reset,
76            grey = grey,
77            gray = gray,
78            white = white,
79            ulid = $self.session_ulid,
80            peer = $self.socket.socket_ref().peer_addr().ok(),
81            position = $self.position,
82            state = $self.state,
83            streams = $self.streams.len(),
84            last_peer_id = $self.highest_peer_stream_id,
85            window = $self.flow_control.window,
86            draining = $self.drain.draining,
87            total_rst_streams_emitted_lifetime = $self.flood_detector.total_rst_streams_emitted_lifetime,
88            total_rst_received_lifetime = $self.flood_detector.total_rst_received_lifetime,
89            readiness = $self.readiness,
90        )
91    }};
92}
93
94/// Per-stream variant of [`log_context!`] used when a [`Stream`]'s
95/// [`HttpContext`](crate::protocol::kawa_h1::editor::HttpContext) is in
96/// scope. Populates the `request_id`, `cluster_id` and `backend_id` slots of
97/// the bracket so the log line can be filtered by the specific H2 stream it
98/// belongs to.
99#[allow(unused_macros)]
100macro_rules! log_context_stream {
101    ($self:expr, $http_context:expr) => {{
102        let (open, reset, grey, gray, white) = ansi_palette();
103        format!(
104            "[{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 >>>",
105            open = open,
106            reset = reset,
107            grey = grey,
108            gray = gray,
109            white = white,
110            ulid = $self.session_ulid,
111            req = $http_context.id,
112            cluster = $http_context.cluster_id.as_deref().unwrap_or("-"),
113            backend = $http_context.backend_id.as_deref().unwrap_or("-"),
114            peer = $self.socket.socket_ref().peer_addr().ok(),
115            position = $self.position,
116            state = $self.state,
117            streams = $self.streams.len(),
118            last_peer_id = $self.highest_peer_stream_id,
119            window = $self.flow_control.window,
120            draining = $self.drain.draining,
121            total_rst_streams_emitted_lifetime = $self.flood_detector.total_rst_streams_emitted_lifetime,
122            total_rst_received_lifetime = $self.flood_detector.total_rst_received_lifetime,
123            readiness = $self.readiness,
124        )
125    }};
126}
127
128/// Module-level prefix without session context, for logs emitted from
129/// free functions, `H2ConnectionConfig` validation and other sites where no
130/// `ConnectionH2` is in scope. Keeps the `MUX-H2` label consistent with
131/// connection logs and honours the colored flag.
132macro_rules! log_module_context {
133    () => {{
134        let (open, reset, _, _, _) = ansi_palette();
135        format!("{open}MUX-H2{reset}\t >>>", open = open, reset = reset)
136    }};
137}
138
139/// `if let Some(violation) = self.flood_detector.check_flood() { return self.handle_flood_violation(violation); }`
140/// pattern wrapped as a single statement. Pure dispatch — the actual flood
141/// thresholds and counters live inside `H2FloodDetector::check_flood` and
142/// `ConnectionH2::handle_flood_violation`, which the macro does not touch.
143/// Use this at every per-frame counter bump site so the wrapper stays
144/// uniform and a future grep for "flood-check forgot to return" finds zero.
145macro_rules! check_flood_or_return {
146    ($self:expr) => {
147        if let Some(violation) = $self.flood_detector.check_flood() {
148            return $self.handle_flood_violation(violation);
149        }
150    };
151}
152
153/// Outcome of a single-stream write flush in write_streams.
154#[derive(Debug, Clone, Copy, PartialEq, Eq)]
155enum FlushOutcome {
156    /// All queued bytes were drained to the socket.
157    Drained,
158    /// The socket blocked before the queue was drained. The caller must
159    /// arrange to resume (set expect_write or return from write_streams).
160    Stalled,
161}
162
163// ── RFC 9113 §6.5.2 Settings Defaults ───────────────────────────────────────
164
165const DEFAULT_HEADER_TABLE_SIZE: u32 = 4096;
166const DEFAULT_MAX_CONCURRENT_STREAMS: u32 = 100;
167pub(super) const DEFAULT_INITIAL_WINDOW_SIZE: u32 = (1 << 16) - 1; // 65535
168const DEFAULT_MAX_FRAME_SIZE: u32 = 1 << 14; // 16384
169
170// RFC 9113 §6.5.2: SETTINGS_MAX_FRAME_SIZE valid range [2^14, 2^24)
171const MIN_MAX_FRAME_SIZE: u32 = 1 << 14; // 16384
172const MAX_MAX_FRAME_SIZE: u32 = 1 << 24; // 16777216 (exclusive upper bound)
173
174// RFC 9113 §6.9: maximum flow control window size (2^31 - 1)
175const FLOW_CONTROL_MAX_WINDOW: u32 = (1 << 31) - 1;
176// RFC 9113 §5.1.1: stream identifiers are 31-bit unsigned integers (2^31 - 1).
177const STREAM_ID_MAX: u32 = 0x7FFF_FFFF;
178
179/// Allocate the next locally-initiated stream identifier given the current
180/// `last_stream_id` watermark, returning `(issued_id, next_last_stream_id)`
181/// or `None` when the 31-bit space is exhausted.
182///
183/// RFC 9113 §5.1.1 reserves odd identifiers for clients and even identifiers
184/// for servers. Sōzu never server-pushes, so in practice this helper is
185/// called on the backend (client) side via [`ConnectionH2::new_stream_id`].
186/// The server branch is kept symmetrical so the behaviour is exercised by
187/// the unit tests and remains correct if push is ever enabled.
188///
189/// `last_stream_id` tracks the even "watermark" (2, 4, 6, ...). A client call
190/// issues `watermark - 1` (odd), a server call issues `watermark - 2` (even).
191/// The helper enforces two invariants:
192/// - the issued identifier never exceeds `STREAM_ID_MAX` (2³¹ - 1); and
193/// - the returned watermark is a valid starting point for the next call.
194///
195/// Exhaustion is reported with `None` to the caller, which must emit
196/// GOAWAY(NO_ERROR) and stop issuing new streams on this connection
197/// (see `start_stream` for the client-side drain path).
198pub(super) fn next_stream_id(
199    last_stream_id: StreamId,
200    is_client: bool,
201) -> Option<(StreamId, StreamId)> {
202    let next = last_stream_id.checked_add(2)?;
203    let issued = if is_client {
204        next.checked_sub(1)?
205    } else {
206        next.checked_sub(2)?
207    };
208    // RFC 9113 §5.1.1: stream identifiers are 31-bit. Reject any allocation
209    // whose issued value would exceed `STREAM_ID_MAX`; the watermark itself
210    // is allowed to sit at `STREAM_ID_MAX + 1` (the sentinel that fails the
211    // next call).
212    if issued > STREAM_ID_MAX {
213        return None;
214    }
215    // Post-conditions (RFC 9113 §5.1.1):
216    // - the issued id fits the 31-bit space;
217    // - the returned watermark is strictly greater than the id we issued, so a
218    //   subsequent call cannot re-issue or regress;
219    // - role-parity: client ids are odd, server ids even. This holds ONLY when
220    //   `last_stream_id` is an even watermark, which is the helper's documented
221    //   contract and what production always maintains (`create_stream` rounds to
222    //   `(stream_id + 2) & !1`; the connection initialises it to 0). The unit
223    //   tests deliberately feed odd `last` values at the saturation boundary, so
224    //   the parity check is gated on the watermark being even — a parity slip
225    //   from an *even* watermark would let two roles collide on one id.
226    debug_assert!(
227        issued <= STREAM_ID_MAX,
228        "issued stream id must fit the 31-bit space"
229    );
230    debug_assert!(
231        next > issued,
232        "the next watermark must advance strictly past the issued id"
233    );
234    debug_assert!(
235        last_stream_id & 1 != 0 || (issued & 1 == 1) == is_client,
236        "from an even watermark, client ids must be odd and server ids even (RFC 9113 §5.1.1)"
237    );
238    Some((issued, next))
239}
240
241/// Enlarged connection-level receive window (1 MB).
242/// The RFC 9113 default is 65 535 bytes, which is too small for high-throughput
243/// proxying and causes excessive WINDOW_UPDATE round-trips. 1 MB matches the
244/// initial window used by HAProxy, the h2 crate, and other production proxies.
245const ENLARGED_CONNECTION_WINDOW: u32 = 1_048_576;
246
247/// H2 client connection preface size: 24-byte magic + 9-byte SETTINGS frame header
248pub(super) const CLIENT_PREFACE_SIZE: usize = 24 + parser::FRAME_HEADER_SIZE;
249
250// ── Flood Detection Thresholds (CVE mitigations) ────────────────────────────
251
252/// Default maximum RST_STREAM frames per window (CVE-2023-44487 Rapid Reset + CVE-2019-9514)
253const DEFAULT_MAX_RST_STREAM_PER_WINDOW: u32 = 100;
254/// Hard lifetime cap on total RST_STREAM frames received on a single
255/// connection (CVE-2023-44487 Rapid Reset).
256///
257/// The per-window counter half-decays, which allows a patient attacker to
258/// sustain ~50 RST/sec indefinitely — each one costs the backend a request
259/// that will be cancelled before any response work is produced. A lifetime
260/// counter that never decays puts an absolute ceiling on that amplification
261/// per connection. 10 000 is generous for legitimate traffic (months of
262/// occasional client-side cancellations) but rapidly trips on the ~30/sec
263/// abusive pace reported in the CVE-2023-44487 advisory (~5 minutes).
264pub(super) const DEFAULT_MAX_RST_STREAM_LIFETIME: u64 = 10_000;
265/// Hard lifetime cap on RST_STREAM frames received BEFORE the corresponding
266/// backend response has started. These are the cheap-for-client /
267/// expensive-for-us resets that characterise Rapid Reset: the client pays
268/// one RST frame, we pay a round-trip to the backend plus request parsing.
269/// A much lower ceiling kills the attack well before 10 000 lifetime total.
270pub(super) const DEFAULT_MAX_RST_STREAM_ABUSIVE_LIFETIME: u64 = 50;
271/// Absolute lifetime cap on **server-emitted** RST_STREAM frames on a single
272/// connection (CVE-2025-8671 — "MadeYouReset"). Distinct from
273/// [`DEFAULT_MAX_RST_STREAM_LIFETIME`] which caps *received* RSTs
274/// (CVE-2023-44487 Rapid Reset).
275///
276/// MadeYouReset has the server talk itself into flooding: the attacker sends
277/// legitimate-looking frames that force the server to emit RST_STREAM (content
278/// -length mismatch, header parse error, rejected priority, zero-increment
279/// `WINDOW_UPDATE` on an open stream, …). Each forced RST costs the server a
280/// header-decode, kawa buffer setup and frame serialisation; uncapped, it
281/// becomes the same class of DoS as Rapid Reset but with a flipped emission
282/// direction.
283///
284/// 500 is conservative: legitimate traffic very rarely triggers a
285/// server-initiated RST (aside from graceful `NoError` cancels which are not
286/// counted), so crossing 500 on a single connection is a strong abuse signal.
287pub(super) const DEFAULT_MAX_RST_STREAM_EMITTED_LIFETIME: u64 = 500;
288/// Default maximum PING frames per window (CVE-2019-9512 Ping Flood)
289const DEFAULT_MAX_PING_PER_WINDOW: u32 = 100;
290/// Absolute lifetime cap on PING frames received on a single connection.
291/// Mirrors DEFAULT_MAX_RST_STREAM_LIFETIME — generous for legitimate
292/// keep-alives but trips on sustained low-rate abuse (CVE-2019-9512).
293const DEFAULT_MAX_PING_LIFETIME: u32 = 10_000;
294/// Default maximum SETTINGS frames per window (CVE-2019-9515 Settings Flood)
295const DEFAULT_MAX_SETTINGS_PER_WINDOW: u32 = 50;
296/// Absolute lifetime cap on SETTINGS frames received on a single connection.
297/// Mirrors DEFAULT_MAX_RST_STREAM_LIFETIME — generous for legitimate
298/// renegotiations but trips on sustained low-rate abuse (CVE-2019-9515).
299const DEFAULT_MAX_SETTINGS_LIFETIME: u32 = 10_000;
300/// Default maximum empty DATA frames per window (CVE-2019-9518 Empty Frames)
301const DEFAULT_MAX_EMPTY_DATA_PER_WINDOW: u32 = 100;
302/// Default maximum connection-level (stream 0) WINDOW_UPDATE frames per
303/// sliding window. Non-zero stream-0 WINDOW_UPDATE frames are otherwise
304/// uncounted by the generic glitch detector — a peer could burn proxy CPU by
305/// sending millions of legal-looking stream-0 WINDOW_UPDATEs. Value mirrors
306/// [`DEFAULT_MAX_EMPTY_DATA_PER_WINDOW`] / [`DEFAULT_MAX_PING_PER_WINDOW`] —
307/// legitimate proxies only need a handful per second.
308const DEFAULT_MAX_WINDOW_UPDATE_STREAM0_PER_WINDOW: u32 = 100;
309/// Default maximum CONTINUATION frames per header block (CVE-2024-27316)
310const DEFAULT_MAX_CONTINUATION_FRAMES: u32 = 20;
311/// Maximum accumulated header block size across CONTINUATION frames (64KB)
312pub(super) const MAX_HEADER_LIST_SIZE: usize = 65536;
313/// Default maximum HPACK dynamic table size (SETTINGS_HEADER_TABLE_SIZE)
314/// accepted from the peer. 64 KB is well above the RFC default of 4 KB
315/// while preventing a malicious peer from advertising up to 4 GB.
316const DEFAULT_MAX_HEADER_TABLE_SIZE: u32 = 65536;
317/// Default maximum number of materialized header fields per request/response —
318/// HPACK fields plus expanded cookie crumbs (RFC 9113 §8.2.3). Bounds the HPACK
319/// indexed-reference "header bomb": each 1-byte indexed reference materializes a
320/// `Pair` of per-entry bookkeeping, so an attacker amplifies wire bytes into
321/// allocation. RFC 9113 §6.5.2's +32-octet/field accounting alone caps this at
322/// ~2048 fields for a 64 KB list; this explicit count cap is the tighter,
323/// upstream-matching defense (cf. nginx `max_headers`, Apache `LimitRequestFields`).
324const DEFAULT_MAX_HEADER_FIELDS: u32 = 128;
325/// Cumulative outbound progress (bytes) a window-stalled stream must drain to
326/// clear its flow-control-stall deadline (M2 cumulative-stall budget). Below
327/// this, a `WINDOW_UPDATE(+1)` drip that trickles a few bytes per idle period
328/// cannot keep the slot alive: the deadline ages out and the reaper
329/// RST(CANCEL)s the stream. Chosen as one max H2 DATA frame payload (16 KiB) —
330/// a legitimate slow-but-steady transfer drains at least one frame per idle
331/// period at any realistic bandwidth, while a drip attacker grants far less. A
332/// `const`, not a config knob: `h2_stream_idle_timeout_seconds` is already the
333/// operator dial for slow-link tolerance, and coupling a second knob invites
334/// misconfiguration (high floor + low deadline = mass false reaps).
335const FC_STALL_CLEAR_FLOOR: usize = 16 * 1024;
336/// RFC 9113 §6.5.2: the size accounted against `SETTINGS_MAX_HEADER_LIST_SIZE`
337/// is the uncompressed name + value octets PLUS a 32-octet overhead per field.
338/// The per-field overhead is what bounds the field count under a fixed byte
339/// budget — omitting it lets a peer materialize ~33× more fields than intended.
340pub(super) const HEADER_FIELD_SIZE_OVERHEAD: usize = 32;
341/// Duration of the sliding window for rate-based flood counters
342const FLOOD_WINDOW_DURATION: std::time::Duration = std::time::Duration::from_secs(1);
343/// Default maximum general anomaly count before triggering ENHANCE_YOUR_CALM
344const DEFAULT_MAX_GLITCH_COUNT: u32 = 100;
345
346/// RFC 9113 §5.1.2: threshold of `REFUSED_STREAM` emissions per
347/// [`BACKPRESSURE_WINDOW_DURATION`] that triggers back-pressure — at this
348/// point we halve the advertised `SETTINGS_MAX_CONCURRENT_STREAMS` so the
349/// peer throttles its request rate instead of paying the RST round-trip for
350/// every new stream.
351const BACKPRESSURE_REFUSAL_THRESHOLD: u32 = 50;
352/// Sliding window used to detect refusal bursts for SETTINGS back-pressure.
353const BACKPRESSURE_WINDOW_DURATION: std::time::Duration = std::time::Duration::from_secs(60);
354
355/// Configurable thresholds for H2 flood detection.
356///
357/// All values have safe defaults matching the compile-time constants.
358/// When configured via listener config, `None` values fall back to these defaults.
359#[derive(Debug, Clone, Copy, PartialEq, Eq)]
360pub struct H2FloodConfig {
361    /// Maximum RST_STREAM frames per second window (CVE-2023-44487, CVE-2019-9514)
362    pub max_rst_stream_per_window: u32,
363    /// Maximum PING frames per second window (CVE-2019-9512)
364    pub max_ping_per_window: u32,
365    /// Maximum SETTINGS frames per second window (CVE-2019-9515)
366    pub max_settings_per_window: u32,
367    /// Maximum empty DATA frames per second window (CVE-2019-9518)
368    pub max_empty_data_per_window: u32,
369    /// Maximum connection-level (stream 0) WINDOW_UPDATE frames per sliding
370    /// window. Caps the CPU cost of a peer sending a flood of non-zero
371    /// stream-0 WINDOW_UPDATEs — each is individually legal so the generic
372    /// glitch counter does not trip, yet millions per connection still burn
373    /// server CPU parsing and updating the flow window.
374    pub max_window_update_stream0_per_window: u32,
375    /// Maximum CONTINUATION frames per header block (CVE-2024-27316)
376    pub max_continuation_frames: u32,
377    /// Maximum accumulated protocol anomalies before ENHANCE_YOUR_CALM
378    pub max_glitch_count: u32,
379    /// Absolute lifetime cap on RST_STREAM frames received on a single
380    /// connection (CVE-2023-44487). Never decays — provides a ceiling the
381    /// per-window counter cannot.
382    pub max_rst_stream_lifetime: u64,
383    /// Lifetime cap on "abusive" (pre-response-start) RST_STREAM frames —
384    /// the Rapid Reset signature (CVE-2023-44487).
385    pub max_rst_stream_abusive_lifetime: u64,
386    /// Absolute lifetime cap on **server-emitted** RST_STREAM frames for this
387    /// connection (CVE-2025-8671 "MadeYouReset"). Only non-`NoError` resets
388    /// count — graceful cancels are exempt.
389    pub max_rst_stream_emitted_lifetime: u64,
390    /// Maximum accumulated HPACK-decoded header list size per request
391    /// (SETTINGS_MAX_HEADER_LIST_SIZE, RFC 9113 §6.5.2).
392    pub max_header_list_size: u32,
393    /// Maximum HPACK dynamic table size (SETTINGS_HEADER_TABLE_SIZE) accepted
394    /// from the peer. Caps the value the peer advertises in SETTINGS frames to
395    /// prevent unbounded HPACK encoder memory growth.
396    pub max_header_table_size: u32,
397    /// Maximum number of materialized header fields, enforced per HEADERS block
398    /// and (independently) per trailers block — HPACK fields plus expanded
399    /// cookie crumbs (RFC 9113 §8.2.3). Bounds the HPACK indexed-reference
400    /// header bomb, where many 1-byte indexed references each materialize a
401    /// `Pair` of per-entry bookkeeping.
402    pub max_header_fields: u32,
403}
404
405impl Default for H2FloodConfig {
406    fn default() -> Self {
407        Self {
408            max_rst_stream_per_window: DEFAULT_MAX_RST_STREAM_PER_WINDOW,
409            max_ping_per_window: DEFAULT_MAX_PING_PER_WINDOW,
410            max_settings_per_window: DEFAULT_MAX_SETTINGS_PER_WINDOW,
411            max_empty_data_per_window: DEFAULT_MAX_EMPTY_DATA_PER_WINDOW,
412            max_window_update_stream0_per_window: DEFAULT_MAX_WINDOW_UPDATE_STREAM0_PER_WINDOW,
413            max_continuation_frames: DEFAULT_MAX_CONTINUATION_FRAMES,
414            max_glitch_count: DEFAULT_MAX_GLITCH_COUNT,
415            max_rst_stream_lifetime: DEFAULT_MAX_RST_STREAM_LIFETIME,
416            max_rst_stream_abusive_lifetime: DEFAULT_MAX_RST_STREAM_ABUSIVE_LIFETIME,
417            max_rst_stream_emitted_lifetime: DEFAULT_MAX_RST_STREAM_EMITTED_LIFETIME,
418            max_header_list_size: MAX_HEADER_LIST_SIZE as u32,
419            max_header_table_size: DEFAULT_MAX_HEADER_TABLE_SIZE,
420            max_header_fields: DEFAULT_MAX_HEADER_FIELDS,
421        }
422    }
423}
424
425impl H2FloodConfig {
426    /// Create a validated config, clamping all thresholds to at least 1.
427    /// Zero thresholds would cause immediate flood detection on any frame.
428    #[allow(clippy::too_many_arguments)]
429    pub fn new(
430        max_rst_stream_per_window: u32,
431        max_ping_per_window: u32,
432        max_settings_per_window: u32,
433        max_empty_data_per_window: u32,
434        max_window_update_stream0_per_window: u32,
435        max_continuation_frames: u32,
436        max_glitch_count: u32,
437        max_rst_stream_lifetime: u64,
438        max_rst_stream_abusive_lifetime: u64,
439        max_rst_stream_emitted_lifetime: u64,
440        max_header_list_size: u32,
441        max_header_table_size: u32,
442        max_header_fields: u32,
443    ) -> Self {
444        let config = Self {
445            max_rst_stream_per_window: max_rst_stream_per_window.max(1),
446            max_ping_per_window: max_ping_per_window.max(1),
447            max_settings_per_window: max_settings_per_window.max(1),
448            max_empty_data_per_window: max_empty_data_per_window.max(1),
449            max_window_update_stream0_per_window: max_window_update_stream0_per_window.max(1),
450            max_continuation_frames: max_continuation_frames.max(1),
451            max_glitch_count: max_glitch_count.max(1),
452            max_rst_stream_lifetime: max_rst_stream_lifetime.max(1),
453            max_rst_stream_abusive_lifetime: max_rst_stream_abusive_lifetime.max(1),
454            max_rst_stream_emitted_lifetime: max_rst_stream_emitted_lifetime.max(1),
455            max_header_list_size: max_header_list_size.max(1),
456            max_header_table_size: max_header_table_size.max(1),
457            max_header_fields: max_header_fields.max(1),
458        };
459        // Post-condition: every threshold is clamped to at least 1. A zero
460        // threshold would make `check_flood`/`record_rst_*` trip on the very
461        // first frame (count > 0 > threshold), turning a legitimate connection
462        // into an immediate GOAWAY. This is the central invariant the clamps
463        // above exist to enforce — assert it rather than trusting the `.max(1)`
464        // chain stays correct under future edits.
465        debug_assert!(
466            config.max_rst_stream_per_window >= 1
467                && config.max_ping_per_window >= 1
468                && config.max_settings_per_window >= 1
469                && config.max_empty_data_per_window >= 1
470                && config.max_window_update_stream0_per_window >= 1
471                && config.max_continuation_frames >= 1
472                && config.max_glitch_count >= 1,
473            "every u32 flood threshold must be clamped to >= 1"
474        );
475        debug_assert!(
476            config.max_rst_stream_lifetime >= 1
477                && config.max_rst_stream_abusive_lifetime >= 1
478                && config.max_rst_stream_emitted_lifetime >= 1
479                && config.max_header_list_size >= 1
480                && config.max_header_table_size >= 1
481                && config.max_header_fields >= 1,
482            "every lifetime/size flood threshold must be clamped to >= 1"
483        );
484        config
485    }
486}
487
488/// Default stream Vec shrink ratio: shrink when total > active * ratio.
489const DEFAULT_STREAM_SHRINK_RATIO: u32 = 2;
490
491/// Configurable H2 connection tuning parameters.
492///
493/// All values have safe defaults. When configured via listener config,
494/// absent values fall back to compile-time defaults.
495#[derive(Debug, Clone, Copy, PartialEq, Eq)]
496pub struct H2ConnectionConfig {
497    /// Connection-level receive window size in bytes (RFC 9113 §6.9.2).
498    pub initial_connection_window: u32,
499    /// Maximum concurrent streams (SETTINGS_MAX_CONCURRENT_STREAMS).
500    pub max_concurrent_streams: u32,
501    /// Shrink threshold ratio for recycled stream slots.
502    pub stream_shrink_ratio: u32,
503}
504
505impl Default for H2ConnectionConfig {
506    fn default() -> Self {
507        Self {
508            initial_connection_window: ENLARGED_CONNECTION_WINDOW,
509            max_concurrent_streams: DEFAULT_MAX_CONCURRENT_STREAMS,
510            stream_shrink_ratio: DEFAULT_STREAM_SHRINK_RATIO,
511        }
512    }
513}
514
515impl H2ConnectionConfig {
516    /// Create a validated config, clamping to safe bounds.
517    ///
518    /// - `initial_connection_window`: clamped to \[65535, 2^31-1\] per RFC 9113 §6.9
519    /// - `max_concurrent_streams`: minimum 1
520    /// - `stream_shrink_ratio`: minimum 2 (1 would defeat slot recycling)
521    pub fn new(
522        initial_connection_window: u32,
523        max_concurrent_streams: u32,
524        stream_shrink_ratio: u32,
525    ) -> Self {
526        let clamped_window =
527            initial_connection_window.clamp(DEFAULT_INITIAL_WINDOW_SIZE, FLOW_CONTROL_MAX_WINDOW);
528        if clamped_window != initial_connection_window {
529            warn!(
530                "{} h2_initial_connection_window {} clamped to [{}, {}]",
531                log_module_context!(),
532                initial_connection_window,
533                DEFAULT_INITIAL_WINDOW_SIZE,
534                FLOW_CONTROL_MAX_WINDOW
535            );
536        }
537        const MAX_SAFE_CONCURRENT_STREAMS: u32 = 10_000;
538        let clamped_streams = max_concurrent_streams.clamp(1, MAX_SAFE_CONCURRENT_STREAMS);
539        if max_concurrent_streams > MAX_SAFE_CONCURRENT_STREAMS {
540            error!(
541                "{} h2_max_concurrent_streams={} exceeds safe limit, clamped to {}",
542                log_module_context!(),
543                max_concurrent_streams,
544                MAX_SAFE_CONCURRENT_STREAMS
545            );
546        }
547        if clamped_streams != max_concurrent_streams
548            && max_concurrent_streams <= MAX_SAFE_CONCURRENT_STREAMS
549        {
550            warn!(
551                "{} h2_max_concurrent_streams {} clamped to minimum 1",
552                log_module_context!(),
553                max_concurrent_streams
554            );
555        }
556        let clamped_ratio = stream_shrink_ratio.max(2);
557        if clamped_ratio != stream_shrink_ratio {
558            warn!(
559                "{} h2_stream_shrink_ratio {} clamped to minimum 2",
560                log_module_context!(),
561                stream_shrink_ratio
562            );
563        }
564        let config = Self {
565            initial_connection_window: clamped_window,
566            max_concurrent_streams: clamped_streams,
567            stream_shrink_ratio: clamped_ratio,
568        };
569        // Post-conditions matching the documented clamp ranges. The window must
570        // stay within RFC 9113 §6.9's [65535, 2^31-1] (a window outside this
571        // band desynchronises flow control with the peer); max_concurrent_streams
572        // must be >= 1 (zero would refuse every stream); shrink_ratio must be
573        // >= 2 (1 defeats slot recycling, the whole point of the knob).
574        debug_assert!(
575            (DEFAULT_INITIAL_WINDOW_SIZE..=FLOW_CONTROL_MAX_WINDOW)
576                .contains(&config.initial_connection_window),
577            "clamped connection window must lie within RFC 9113 §6.9 bounds"
578        );
579        debug_assert!(
580            config.max_concurrent_streams >= 1,
581            "clamped max_concurrent_streams must be >= 1"
582        );
583        debug_assert!(
584            config.stream_shrink_ratio >= 2,
585            "clamped stream_shrink_ratio must be >= 2 to keep slot recycling effective"
586        );
587        config
588    }
589
590    /// Create from optional config values, falling back to compile-time defaults.
591    /// Combines unwrap-or-default with validation clamping.
592    pub fn from_optional(
593        window: Option<u32>,
594        max_streams: Option<u32>,
595        shrink_ratio: Option<u32>,
596    ) -> Self {
597        let defaults = Self::default();
598        Self::new(
599            window.unwrap_or(defaults.initial_connection_window),
600            max_streams.unwrap_or(defaults.max_concurrent_streams),
601            shrink_ratio.unwrap_or(defaults.stream_shrink_ratio),
602        )
603    }
604}
605
606/// Default pending WINDOW_UPDATE capacity (used in tests).
607/// The actual per-connection cap is computed from `connection_config.max_concurrent_streams`.
608#[cfg(test)]
609const DEFAULT_MAX_PENDING_WINDOW_UPDATES: usize = 1 + DEFAULT_MAX_CONCURRENT_STREAMS as usize * 4;
610
611/// Maximum number of pending RST_STREAM frames before triggering GOAWAY.
612/// When a peer causes excessive RST_STREAM queueing (e.g. rapid stream creation
613/// beyond MAX_CONCURRENT_STREAMS), this cap prevents unbounded memory growth
614/// and triggers an ENHANCE_YOUR_CALM connection error.
615const MAX_PENDING_RST_STREAMS: usize = 200;
616
617/// RFC 9113 §6.5: maximum time (in seconds) to wait for SETTINGS ACK before
618/// sending GOAWAY with SETTINGS_TIMEOUT error code.
619const SETTINGS_ACK_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
620
621#[inline(always)]
622fn error_nom_to_h2(error: nom::Err<parser::ParserError>) -> H2Error {
623    match error {
624        nom::Err::Error(parser::ParserError {
625            kind: parser::ParserErrorKind::H2(e),
626            ..
627        }) => e,
628        nom::Err::Failure(parser::ParserError {
629            kind: parser::ParserErrorKind::H2(e),
630            ..
631        }) => e,
632        _ => H2Error::ProtocolError,
633    }
634}
635
636/// Distribute connection-level byte overhead proportionally to a single stream.
637///
638/// Overhead is distributed in proportion to the bytes this stream transferred
639/// relative to the total across all active streams. A stream that transferred
640/// 60% of total bytes gets 60% of the overhead.
641///
642/// `stream_bytes` and `total_bytes` are `(bytes_in, bytes_out)` tuples.
643/// Falls back to even distribution (1/active_streams) when no stream has
644/// transferred any bytes yet (total is zero).
645///
646/// Extracted as a free function to avoid borrow conflicts when `self` fields
647/// (e.g. `encoder`) are borrowed by the converter while we need to update
648/// per-stream metrics and connection overhead counters.
649fn distribute_overhead(
650    metrics: &mut SessionMetrics,
651    overhead_bin: &mut usize,
652    overhead_bout: &mut usize,
653    stream_bytes: (usize, usize),
654    total_bytes: (usize, usize),
655    active_streams: usize,
656    is_last_stream: bool,
657) {
658    let share_in = if is_last_stream {
659        // Last stream gets all remaining overhead to avoid losing remainder bytes
660        // from integer division across earlier streams.
661        *overhead_bin
662    } else if total_bytes.0 > 0 {
663        // Clamp to remaining overhead — integer division rounding across multiple
664        // streams can cause accumulated shares to exceed the total.
665        (*overhead_bin * stream_bytes.0 / total_bytes.0).min(*overhead_bin)
666    } else {
667        // No stream has transferred any inbound bytes — fall back to even split.
668        *overhead_bin / active_streams.max(1)
669    };
670    let share_out = if is_last_stream {
671        *overhead_bout
672    } else if total_bytes.1 > 0 {
673        (*overhead_bout * stream_bytes.1 / total_bytes.1).min(*overhead_bout)
674    } else {
675        // No stream has transferred any outbound bytes — fall back to even split.
676        *overhead_bout / active_streams.max(1)
677    };
678    // Pre-condition: a stream can never be credited more overhead than remains
679    // in the pool — otherwise the `*overhead_b* -= share_*` below underflows
680    // (usize wraps to a huge value, corrupting connection-overhead accounting).
681    // Every branch above either takes the whole pool (last stream) or `.min`s
682    // against it, so this must hold.
683    debug_assert!(
684        share_in <= *overhead_bin,
685        "overhead-in share must not exceed the remaining overhead pool"
686    );
687    debug_assert!(
688        share_out <= *overhead_bout,
689        "overhead-out share must not exceed the remaining overhead pool"
690    );
691    let before_bin = *overhead_bin;
692    let before_bout = *overhead_bout;
693    metrics.bin += share_in;
694    metrics.bout += share_out;
695    *overhead_bin -= share_in;
696    *overhead_bout -= share_out;
697    // Post-condition: the pool shrinks by exactly the credited share (overhead
698    // is conserved, neither created nor lost). The last stream drains it to 0.
699    debug_assert_eq!(
700        *overhead_bin,
701        before_bin - share_in,
702        "overhead-in pool must decrease by exactly the credited share"
703    );
704    debug_assert_eq!(
705        *overhead_bout,
706        before_bout - share_out,
707        "overhead-out pool must decrease by exactly the credited share"
708    );
709    debug_assert!(
710        !is_last_stream || (*overhead_bin == 0 && *overhead_bout == 0),
711        "the last stream must drain the overhead pool to zero (no lost remainder)"
712    );
713}
714
715/// LIFECYCLE §9 invariant 16 probe: returns `true` if any open stream still
716/// has outbound kawa bytes queued (`back.out` non-empty or `back.blocks`
717/// non-drained).
718///
719/// Used by `finalize_write` to preserve `Ready::WRITABLE` across a voluntary
720/// scheduler yield, and by `has_pending_write_full` to block shutdown-drain
721/// while bytes are still owed to the frontend.
722///
723/// `.get()` rather than direct indexing: an unknown `GlobalStreamId` is
724/// treated as "no pending bytes" rather than panicking — defence-in-depth
725/// against a stream-removal race during shutdown.
726fn any_stream_has_pending_back(
727    streams: &HashMap<StreamId, GlobalStreamId>,
728    context_streams: &[Stream],
729) -> bool {
730    any_stream_id_matches(streams, |gid| {
731        context_streams
732            .get(gid)
733            .is_some_and(|s| !s.back.out.is_empty() || !s.back.blocks.is_empty())
734    })
735}
736
737/// Iteration core of [`any_stream_has_pending_back`], split out so the
738/// invariant-16 dispatch is unit-testable without a full [`Stream`] fixture
739/// (the existing test module only covers `H2FloodDetector`).
740fn any_stream_id_matches<F>(streams: &HashMap<StreamId, GlobalStreamId>, mut probe: F) -> bool
741where
742    F: FnMut(GlobalStreamId) -> bool,
743{
744    streams.values().any(|gid| probe(*gid))
745}
746
747/// Collect the live streams that have exceeded `deadline` under either
748/// per-stream reap guard, deduped so a stream tripping both is reaped (and
749/// access-logged) exactly once. Split out from
750/// [`ConnectionH2::cancel_timed_out_streams`] so the two-guard union is
751/// unit-testable without a full `ConnectionH2` fixture (the existing test
752/// module only fixtures `H2FloodDetector` and `Stream`):
753///
754/// - `last_activity` — bidirectional-silence guard: no DATA/HEADERS in either
755///   direction (the slow-multiplex Slowloris timer).
756/// - `fc_stalled` — outbound-flow-control-starvation guard: a buffered response
757///   that cannot drain because the peer keeps its receive window shut (the
758///   HTTP/2 window-stall / WINDOW_UPDATE-drip vector). This guard is what the
759///   liveness timer misses: an inbound 1-byte DATA drip keeps `last_activity`
760///   warm, but never touches `fc_stalled`.
761///
762/// Streams not in `live_streams` or already in `rst_sent` are skipped. The
763/// returned reason string is the access-log tag for the guard that tripped
764/// first (idle takes precedence on a tie, purely for a stable label).
765fn collect_timed_out_streams(
766    last_activity: &HashMap<StreamId, Instant>,
767    fc_stalled: &HashMap<StreamId, Instant>,
768    live_streams: &HashMap<StreamId, GlobalStreamId>,
769    rst_sent: &HashSet<StreamId>,
770    now: Instant,
771    deadline: std::time::Duration,
772) -> Vec<(StreamId, &'static str)> {
773    let eligible = |sid: StreamId| live_streams.contains_key(&sid) && !rst_sent.contains(&sid);
774    let expired = |t: Instant| now.saturating_duration_since(t) > deadline;
775    let mut seen: HashSet<StreamId> = HashSet::new();
776    let mut out: Vec<(StreamId, &'static str)> = Vec::new();
777    for (&sid, &t) in last_activity {
778        if eligible(sid) && expired(t) && seen.insert(sid) {
779            out.push((sid, "H2::IdleTimeout"));
780        }
781    }
782    for (&sid, &t) in fc_stalled {
783        if eligible(sid) && expired(t) && seen.insert(sid) {
784            out.push((sid, "H2::WindowStall"));
785        }
786    }
787    out
788}
789
790/// True when a stream still has response/upload bytes that could be put on the
791/// wire — headers/body in flight, or a terminated-but-not-fully-flushed buffer.
792/// Deliberately EXCLUDES `is_error()`/`rst_sent`: that disjunct is specific to
793/// the priority-eligibility and write-loop gates (`write_streams`) and must stay
794/// inline there; this 2-clause helper backs ONLY the window-stall arm.
795fn has_sendable_response(kawa: &GenericHttpStream) -> bool {
796    kawa.is_main_phase() || (kawa.is_terminated() && !kawa.is_completed())
797}
798
799/// Outcome of the M2 cumulative-stall budget decision for one `write_streams`
800/// pass on a window-stalled stream. Extracted from the `write_streams` arm so
801/// the budget logic is unit-testable without a full `ConnectionH2` fixture
802/// (mirrors the [`collect_timed_out_streams`] extraction).
803#[derive(Debug, Clone, Copy, PartialEq, Eq)]
804enum FcStallAction {
805    /// Clear both the deadline (`stream_fc_stalled_since`) and the progress
806    /// accumulator (`stream_fc_stalled_progress`) for this stream.
807    Clear,
808    /// Ensure the deadline is armed (WITHOUT refreshing an existing `Instant`)
809    /// and set the progress accumulator to `progress`.
810    Arm { progress: usize },
811}
812
813/// Decide what to do with a stream's flow-control-stall deadline + cumulative
814/// progress accumulator on one write pass (M2 cumulative-stall budget).
815///
816/// - A genuinely open send window (`!outbound_window_blocked`) is a real
817///   un-stall → [`FcStallAction::Clear`].
818/// - While the window stays blocked, accumulate this pass's outbound drain
819///   (`consumed`, clamped to `>= 0`) onto `prior_progress`. Once the cumulative
820///   total reaches [`FC_STALL_CLEAR_FLOOR`] (a full DATA frame of real delivery)
821///   → `Clear`; otherwise `Arm` with the running total. A `WINDOW_UPDATE(+1)`
822///   drip adds ~1 byte/pass and never reaches the floor, so the deadline keeps
823///   aging and the reaper eventually fires.
824fn fc_stall_budget_decision(
825    outbound_window_blocked: bool,
826    consumed: i32,
827    prior_progress: Option<usize>,
828) -> FcStallAction {
829    if !outbound_window_blocked {
830        return FcStallAction::Clear;
831    }
832    let progressed = prior_progress
833        .unwrap_or(0)
834        .saturating_add(consumed.max(0) as usize);
835    if progressed >= FC_STALL_CLEAR_FLOOR {
836        FcStallAction::Clear
837    } else {
838        FcStallAction::Arm {
839            progress: progressed,
840        }
841    }
842}
843
844/// Core of [`ConnectionH2::enqueue_rst`], extracted so the RST-queueing
845/// semantics (dedupe, queued-cap counter bump, invariant-15 readiness rearm)
846/// can be unit-tested without building a full `ConnectionH2<Front>` fixture.
847///
848/// Invariants enforced:
849/// - **Dedupe** via `rst_sent`: at most one queued RST per wire stream id.
850///   `HashSet::insert` returns `false` when the id is already present; we
851///   short-circuit on that branch to keep `pending_rst_streams`,
852///   `total_rst_streams_queued` and the wire counts consistent.
853/// - **MadeYouReset queued cap** (`MAX_PENDING_RST_STREAMS`): each freshly
854///   queued RST bumps `total_rst_streams_queued`, which
855///   `flush_pending_control_frames` polices to escalate to
856///   `GOAWAY(ENHANCE_YOUR_CALM)` when exceeded.
857/// - **Invariant 15** (edge-triggered epoll): pair `Ready::WRITABLE` interest
858///   with the event bit so `writable()` is scheduled on the next tick.
859///
860/// Returns `true` when the RST was freshly queued, `false` when the
861/// stream was already in `rst_sent` (the caller asked to RST the same
862/// stream twice — a benign re-entrant idempotency, NOT a new wire
863/// emission). The boolean lets [`ConnectionH2::enqueue_rst`] account
864/// the RST only on the freshly-queued path so duplicate calls do not
865/// inflate the per-error counter or trip the MadeYouReset flood cap
866/// for frames that never reach the wire.
867fn enqueue_rst_into(
868    pending: &mut Vec<(StreamId, H2Error)>,
869    total: &mut usize,
870    rst_sent: &mut HashSet<StreamId>,
871    readiness: &mut Readiness,
872    wire_stream_id: StreamId,
873    error: H2Error,
874) -> bool {
875    let pending_before = pending.len();
876    let total_before = *total;
877    if !rst_sent.insert(wire_stream_id) {
878        // Dedupe short-circuit: the id was already queued/flushed. We must NOT
879        // touch any of the wire-count state, otherwise duplicate calls inflate
880        // the MadeYouReset (CVE-2025-8671) lifetime cap with frames that never
881        // reach the wire.
882        debug_assert!(
883            rst_sent.contains(&wire_stream_id),
884            "dedupe path requires the id to already be present in rst_sent"
885        );
886        debug_assert_eq!(
887            pending.len(),
888            pending_before,
889            "dedupe path must not enqueue a new pending RST"
890        );
891        debug_assert_eq!(
892            *total, total_before,
893            "dedupe path must not bump the queued-RST lifetime counter"
894        );
895        return false;
896    }
897    pending.push((wire_stream_id, error));
898    *total += 1;
899    readiness.arm_writable();
900    // Post-condition: a freshly-queued RST advances both the pending Vec and the
901    // lifetime counter by exactly one, and the id is now tracked for dedupe.
902    debug_assert!(
903        rst_sent.contains(&wire_stream_id),
904        "freshly-queued RST must be recorded in rst_sent for future dedupe"
905    );
906    debug_assert_eq!(
907        pending.len(),
908        pending_before + 1,
909        "a freshly-queued RST must push exactly one pending entry"
910    );
911    debug_assert_eq!(
912        *total,
913        total_before + 1,
914        "a freshly-queued RST must bump the queued-RST lifetime counter by one"
915    );
916    debug_assert_eq!(
917        pending.last().map(|(id, _)| *id),
918        Some(wire_stream_id),
919        "the just-pushed entry must be the requested wire stream id"
920    );
921    true
922}
923
924/// Detail of a flood-threshold violation returned by
925/// [`H2FloodDetector::check_flood`] and [`H2FloodDetector::record_rst_lifetime`].
926///
927/// Carrying `(reason, count, threshold)` lets the caller emit a session-scoped
928/// log line with full context — the detector itself is connection-agnostic and
929/// never logs.
930#[derive(Debug, Clone, PartialEq)]
931pub struct H2FloodViolation {
932    /// HTTP/2 error code to emit on the GOAWAY.
933    pub error: H2Error,
934    /// Human-readable name of the counter that tripped (e.g. `"RST_STREAM"`).
935    pub reason: &'static str,
936    /// Statsd metric key emitted by [`ConnectionH2::handle_flood_violation`].
937    /// Carried alongside `reason` so a single field maps to both the log line
938    /// and the dashboard counter — adding a new violation kind requires
939    /// choosing both at the construction site, preventing drift.
940    pub metric_key: &'static str,
941    /// Observed counter value at the moment of detection.
942    pub count: u64,
943    /// Configured ceiling that was crossed.
944    pub threshold: u64,
945}
946
947/// Tracks per-connection frame rates to detect and mitigate H2 flood attacks.
948///
949/// Monitors RST_STREAM (CVE-2023-44487), PING (CVE-2019-9512), SETTINGS (CVE-2019-9515),
950/// empty DATA (CVE-2019-9518), and CONTINUATION (CVE-2024-27316) flood patterns.
951/// When any counter exceeds its threshold, `check_flood()` returns the violation
952/// detail so callers can log with connection context before sending GOAWAY.
953///
954/// Thresholds are configurable via [`H2FloodConfig`], with safe defaults matching
955/// the original compile-time constants.
956#[derive(Debug)]
957pub struct H2FloodDetector {
958    /// RST_STREAM frames received in current window (CVE-2023-44487 + CVE-2019-9514)
959    pub(super) rst_stream_count: u32,
960    /// Lifetime RST_STREAM frames received on this connection.
961    ///
962    /// Never decays — provides an absolute ceiling that the half-decaying
963    /// per-window counter cannot, preventing a sustained ~50 RST/sec burst
964    /// from running forever.
965    pub(super) total_rst_received_lifetime: u64,
966    /// Lifetime RST_STREAM frames received that targeted a stream whose
967    /// backend response had not yet started. These are the "Rapid Reset"
968    /// signature — cheap for the attacker, expensive for the proxy — and
969    /// trip on a much lower ceiling than the generic lifetime counter.
970    pub(super) total_abusive_rst_received_lifetime: u64,
971    /// Lifetime RST_STREAM frames **emitted by the server** on this
972    /// connection (CVE-2025-8671 "MadeYouReset" mitigation). Incremented
973    /// inside [`ConnectionH2::reset_stream`] whenever a non-`NoError` reset
974    /// is triggered by an attacker-crafted frame (content-length mismatch,
975    /// header parse error, priority rejection, zero-increment WINDOW_UPDATE
976    /// on an open stream). Never decays — provides an absolute ceiling that
977    /// short-circuits patient-attacker patterns that stay under any windowed
978    /// counter.
979    pub(super) total_rst_streams_emitted_lifetime: u64,
980    /// PING frames received in current window (CVE-2019-9512)
981    pub(super) ping_count: u32,
982    /// Lifetime PING frames received on this connection.
983    ///
984    /// Never decays — provides an absolute ceiling that the half-decaying
985    /// per-window counter cannot, preventing sustained low-rate PING abuse.
986    pub(super) total_ping_received_lifetime: u32,
987    /// SETTINGS frames received in current window (CVE-2019-9515)
988    pub(super) settings_count: u32,
989    /// Lifetime SETTINGS frames received on this connection.
990    ///
991    /// Never decays — provides an absolute ceiling that the half-decaying
992    /// per-window counter cannot, preventing sustained low-rate SETTINGS abuse.
993    pub(super) total_settings_received_lifetime: u32,
994    /// Empty DATA frames received in current window (CVE-2019-9518)
995    pub(super) empty_data_count: u32,
996    /// Connection-level (stream 0) WINDOW_UPDATE frames received in current
997    /// sliding window. Half-decays with [`maybe_reset_window`] like other
998    /// rate counters. Increments on non-zero stream-0 WINDOW_UPDATEs only —
999    /// zero-increment frames short-circuit into GOAWAY(PROTOCOL_ERROR) per
1000    /// RFC 9113 §6.9 before reaching this counter.
1001    pub(super) window_update_stream0_count: u32,
1002    /// CONTINUATION frames received for current header block (CVE-2024-27316)
1003    pub(super) continuation_count: u32,
1004    /// Total accumulated header block size across CONTINUATION frames
1005    pub(super) accumulated_header_size: u32,
1006    /// General anomaly counter
1007    pub(super) glitch_count: u32,
1008    /// Window start for rate-based counters
1009    pub(super) window_start: Instant,
1010    /// Configurable thresholds for flood detection
1011    pub(super) config: H2FloodConfig,
1012}
1013
1014impl Default for H2FloodDetector {
1015    fn default() -> Self {
1016        Self::new(H2FloodConfig::default())
1017    }
1018}
1019
1020impl H2FloodDetector {
1021    pub fn new(config: H2FloodConfig) -> Self {
1022        // Pre-condition: thresholds are already validated (clamped to >= 1 by
1023        // `H2FloodConfig::new`). A zero per-window threshold would trip on the
1024        // first counted frame; assert it here so a config that bypassed `new`
1025        // (raw struct literal in a future caller) is caught in debug.
1026        debug_assert!(
1027            config.max_rst_stream_per_window >= 1
1028                && config.max_ping_per_window >= 1
1029                && config.max_settings_per_window >= 1
1030                && config.max_continuation_frames >= 1
1031                && config.max_glitch_count >= 1,
1032            "flood detector must be constructed with validated (>= 1) thresholds"
1033        );
1034        Self {
1035            rst_stream_count: 0,
1036            total_rst_received_lifetime: 0,
1037            total_abusive_rst_received_lifetime: 0,
1038            total_rst_streams_emitted_lifetime: 0,
1039            ping_count: 0,
1040            total_ping_received_lifetime: 0,
1041            settings_count: 0,
1042            total_settings_received_lifetime: 0,
1043            empty_data_count: 0,
1044            window_update_stream0_count: 0,
1045            continuation_count: 0,
1046            accumulated_header_size: 0,
1047            glitch_count: 0,
1048            window_start: Instant::now(),
1049            config,
1050        }
1051    }
1052
1053    /// Increment the lifetime RST_STREAM counters and return a
1054    /// [`H2FloodViolation`] if either the global or the abusive
1055    /// (pre-response-start) lifetime cap has been exceeded.
1056    ///
1057    /// `response_started` indicates whether the backend response had already
1058    /// begun when the RST arrived; `false` is the cheap-for-client /
1059    /// expensive-for-us Rapid Reset signature (CVE-2023-44487).
1060    pub fn record_rst_lifetime(&mut self, response_started: bool) -> Option<H2FloodViolation> {
1061        let total_before = self.total_rst_received_lifetime;
1062        let abusive_before = self.total_abusive_rst_received_lifetime;
1063        self.total_rst_received_lifetime = self.total_rst_received_lifetime.saturating_add(1);
1064        if !response_started {
1065            self.total_abusive_rst_received_lifetime =
1066                self.total_abusive_rst_received_lifetime.saturating_add(1);
1067        }
1068        // Monotonicity: the global lifetime counter advances by one per call
1069        // (until saturation), and the abusive sub-counter advances iff the RST
1070        // arrived before the backend response started. The abusive counter can
1071        // never exceed the global one — every abusive RST is also a received RST.
1072        debug_assert!(
1073            self.total_rst_received_lifetime >= total_before,
1074            "lifetime RST counter must be monotonic non-decreasing"
1075        );
1076        debug_assert_eq!(
1077            self.total_abusive_rst_received_lifetime > abusive_before,
1078            !response_started,
1079            "abusive RST counter advances iff the RST is pre-response-start"
1080        );
1081        debug_assert!(
1082            self.total_abusive_rst_received_lifetime <= self.total_rst_received_lifetime,
1083            "abusive RST count is a subset of total received RST count"
1084        );
1085        if self.total_rst_received_lifetime > self.config.max_rst_stream_lifetime {
1086            return Some(H2FloodViolation {
1087                error: H2Error::EnhanceYourCalm,
1088                reason: "Rapid Reset: lifetime RST_STREAM",
1089                metric_key: "h2.flood.violation.rst_stream_lifetime",
1090                count: self.total_rst_received_lifetime,
1091                threshold: self.config.max_rst_stream_lifetime,
1092            });
1093        }
1094        if self.total_abusive_rst_received_lifetime > self.config.max_rst_stream_abusive_lifetime {
1095            return Some(H2FloodViolation {
1096                error: H2Error::EnhanceYourCalm,
1097                reason: "Rapid Reset: lifetime pre-response RST_STREAM",
1098                metric_key: "h2.flood.violation.rst_stream_pre_response_lifetime",
1099                count: self.total_abusive_rst_received_lifetime,
1100                threshold: self.config.max_rst_stream_abusive_lifetime,
1101            });
1102        }
1103        None
1104    }
1105
1106    /// Increment the lifetime **server-emitted** RST_STREAM counter and
1107    /// return a [`H2FloodViolation`] once the configured ceiling is exceeded.
1108    ///
1109    /// Call sites are the error paths inside [`ConnectionH2::reset_stream`]
1110    /// where an attacker-crafted frame coerces the server into emitting a
1111    /// RST_STREAM (CVE-2025-8671 "MadeYouReset"). Only non-`NoError` resets
1112    /// are reported — callers must exclude graceful cancels.
1113    pub fn record_rst_emitted(&mut self) -> Option<H2FloodViolation> {
1114        let before = self.total_rst_streams_emitted_lifetime;
1115        self.total_rst_streams_emitted_lifetime =
1116            self.total_rst_streams_emitted_lifetime.saturating_add(1);
1117        // Monotonic: the emitted-RST counter never decays (it is the absolute
1118        // MadeYouReset ceiling, CVE-2025-8671), so each call strictly advances
1119        // it until u64 saturation.
1120        debug_assert!(
1121            self.total_rst_streams_emitted_lifetime > before || before == u64::MAX,
1122            "emitted-RST lifetime counter must advance (or already be saturated)"
1123        );
1124        if self.total_rst_streams_emitted_lifetime > self.config.max_rst_stream_emitted_lifetime {
1125            return Some(H2FloodViolation {
1126                error: H2Error::EnhanceYourCalm,
1127                reason: "MadeYouReset: lifetime server-emitted RST_STREAM",
1128                metric_key: "h2.flood.violation.rst_stream_emitted_lifetime",
1129                count: self.total_rst_streams_emitted_lifetime,
1130                threshold: self.config.max_rst_stream_emitted_lifetime,
1131            });
1132        }
1133        None
1134    }
1135
1136    /// Half-decay rate-based counters if the current window has expired.
1137    /// Uses half-window decay instead of full reset to catch burst-then-wait attacks.
1138    fn maybe_reset_window(&mut self) {
1139        if self.window_start.elapsed() >= FLOOD_WINDOW_DURATION {
1140            let (rst_before, ping_before, settings_before) =
1141                (self.rst_stream_count, self.ping_count, self.settings_count);
1142            let (empty_before, wu0_before, glitch_before) = (
1143                self.empty_data_count,
1144                self.window_update_stream0_count,
1145                self.glitch_count,
1146            );
1147            self.rst_stream_count /= 2;
1148            self.ping_count /= 2;
1149            self.settings_count /= 2;
1150            self.empty_data_count /= 2;
1151            self.window_update_stream0_count /= 2;
1152            self.glitch_count /= 2;
1153            self.window_start = Instant::now();
1154            // Half-decay invariant: each rate-based counter is exactly halved
1155            // (integer division), never increased. Catching burst-then-wait
1156            // attacks relies on the counter shrinking but not vanishing — a
1157            // full reset would let a patient attacker reset to zero each window.
1158            debug_assert_eq!(self.rst_stream_count, rst_before / 2, "RST count halves");
1159            debug_assert_eq!(self.ping_count, ping_before / 2, "PING count halves");
1160            debug_assert_eq!(
1161                self.settings_count,
1162                settings_before / 2,
1163                "SETTINGS count halves"
1164            );
1165            debug_assert_eq!(
1166                self.empty_data_count,
1167                empty_before / 2,
1168                "empty-DATA count halves"
1169            );
1170            debug_assert_eq!(
1171                self.window_update_stream0_count,
1172                wu0_before / 2,
1173                "stream-0 WINDOW_UPDATE count halves"
1174            );
1175            debug_assert_eq!(self.glitch_count, glitch_before / 2, "glitch count halves");
1176            // The lifetime counters are deliberately NOT touched here — they are
1177            // the never-decaying ceilings. Guard against a future edit decaying
1178            // them by accident.
1179            debug_assert!(
1180                self.window_start.elapsed() < FLOOD_WINDOW_DURATION,
1181                "window_start must be refreshed to (approximately) now after decay"
1182            );
1183        }
1184    }
1185
1186    /// Check all flood counters. Returns a [`H2FloodViolation`] when a threshold
1187    /// is exceeded; the caller is responsible for logging with session context
1188    /// and escalating to GOAWAY.
1189    pub fn check_flood(&mut self) -> Option<H2FloodViolation> {
1190        self.maybe_reset_window();
1191
1192        fn flag(
1193            reason: &'static str,
1194            metric_key: &'static str,
1195            count: u32,
1196            threshold: u32,
1197        ) -> Option<H2FloodViolation> {
1198            if count > threshold {
1199                Some(H2FloodViolation {
1200                    error: H2Error::EnhanceYourCalm,
1201                    reason,
1202                    metric_key,
1203                    count: count as u64,
1204                    threshold: threshold as u64,
1205                })
1206            } else {
1207                None
1208            }
1209        }
1210
1211        let violation = flag(
1212            "RST_STREAM",
1213            "h2.flood.violation.rst_stream_window",
1214            self.rst_stream_count,
1215            self.config.max_rst_stream_per_window,
1216        )
1217        .or_else(|| {
1218            flag(
1219                "PING",
1220                "h2.flood.violation.ping_window",
1221                self.ping_count,
1222                self.config.max_ping_per_window,
1223            )
1224        })
1225        .or_else(|| {
1226            flag(
1227                "PING lifetime",
1228                "h2.flood.violation.ping_lifetime",
1229                self.total_ping_received_lifetime,
1230                DEFAULT_MAX_PING_LIFETIME,
1231            )
1232        })
1233        .or_else(|| {
1234            flag(
1235                "SETTINGS",
1236                "h2.flood.violation.settings_window",
1237                self.settings_count,
1238                self.config.max_settings_per_window,
1239            )
1240        })
1241        .or_else(|| {
1242            flag(
1243                "SETTINGS lifetime",
1244                "h2.flood.violation.settings_lifetime",
1245                self.total_settings_received_lifetime,
1246                DEFAULT_MAX_SETTINGS_LIFETIME,
1247            )
1248        })
1249        .or_else(|| {
1250            flag(
1251                "empty DATA",
1252                "h2.flood.violation.empty_data_window",
1253                self.empty_data_count,
1254                self.config.max_empty_data_per_window,
1255            )
1256        })
1257        .or_else(|| {
1258            flag(
1259                "CONTINUATION",
1260                "h2.flood.violation.continuation_per_block",
1261                self.continuation_count,
1262                self.config.max_continuation_frames,
1263            )
1264        })
1265        .or_else(|| {
1266            flag(
1267                "WINDOW_UPDATE stream 0",
1268                "h2.flood.violation.window_update_stream0_window",
1269                self.window_update_stream0_count,
1270                self.config.max_window_update_stream0_per_window,
1271            )
1272        })
1273        .or_else(|| {
1274            flag(
1275                "accumulated header size",
1276                "h2.flood.violation.header_size_per_block",
1277                self.accumulated_header_size,
1278                self.config.max_header_list_size,
1279            )
1280        })
1281        .or_else(|| {
1282            flag(
1283                "glitch",
1284                "h2.flood.violation.glitch_window",
1285                self.glitch_count,
1286                self.config.max_glitch_count,
1287            )
1288        });
1289        // Post-condition: any reported violation is well-formed — every H2
1290        // flood escalation is an ENHANCE_YOUR_CALM connection error, and the
1291        // observed count strictly exceeds the threshold it tripped (the `flag`
1292        // helper and the lifetime checks all use strict `>`). A violation whose
1293        // count <= threshold would be a false positive terminating a healthy
1294        // connection.
1295        debug_assert!(
1296            violation
1297                .as_ref()
1298                .is_none_or(|v| v.error == H2Error::EnhanceYourCalm && v.count > v.threshold),
1299            "a flood violation must be EnhanceYourCalm with count strictly above threshold"
1300        );
1301        violation
1302    }
1303
1304    /// Reset CONTINUATION-specific counters when a header block is complete.
1305    pub fn reset_continuation(&mut self) {
1306        self.continuation_count = 0;
1307        self.accumulated_header_size = 0;
1308        // Post-condition: both CONTINUATION-block accumulators are cleared so
1309        // the next header block starts from zero (CVE-2024-27316 per-block
1310        // accounting must not leak across blocks).
1311        debug_assert_eq!(
1312            self.continuation_count, 0,
1313            "continuation_count must be zero after a block completes"
1314        );
1315        debug_assert_eq!(
1316            self.accumulated_header_size, 0,
1317            "accumulated_header_size must be zero after a block completes"
1318        );
1319    }
1320}
1321
1322#[derive(Debug)]
1323pub enum H2State {
1324    ClientPreface,
1325    ClientSettings,
1326    ServerSettings,
1327    Header,
1328    Frame(FrameHeader),
1329    ContinuationHeader(Headers),
1330    ContinuationFrame(Headers),
1331    GoAway,
1332    Error,
1333    Discard,
1334}
1335
1336#[derive(Debug, Clone, Copy)]
1337pub struct H2Settings {
1338    pub settings_header_table_size: u32,
1339    pub settings_enable_push: bool,
1340    pub settings_max_concurrent_streams: u32,
1341    pub settings_initial_window_size: u32,
1342    pub settings_max_frame_size: u32,
1343    pub settings_max_header_list_size: u32,
1344    /// RFC 8441
1345    pub settings_enable_connect_protocol: bool,
1346    /// RFC 9218
1347    pub settings_no_rfc7540_priorities: bool,
1348}
1349
1350impl Default for H2Settings {
1351    fn default() -> Self {
1352        Self {
1353            settings_header_table_size: DEFAULT_HEADER_TABLE_SIZE,
1354            settings_enable_push: false,
1355            settings_max_concurrent_streams: DEFAULT_MAX_CONCURRENT_STREAMS,
1356            settings_initial_window_size: DEFAULT_INITIAL_WINDOW_SIZE,
1357            settings_max_frame_size: DEFAULT_MAX_FRAME_SIZE,
1358            settings_max_header_list_size: MAX_HEADER_LIST_SIZE as u32,
1359            settings_enable_connect_protocol: false,
1360            settings_no_rfc7540_priorities: true,
1361        }
1362    }
1363}
1364
1365/// RFC 9218 Extensible Priorities for HTTP stream scheduling.
1366///
1367/// Stores per-stream urgency (0-7, lower = more important) and incremental
1368/// flag. Used by `writable()` to sort streams: lower urgency first, then
1369/// stream ID for stability among same-urgency non-incremental streams.
1370///
1371/// Within a same-urgency bucket the scheduler (see
1372/// [`ConnectionH2::write_streams`]) drains non-incremental streams
1373/// sequentially, then applies RFC 9218 §4 round-robin to the incremental
1374/// streams starting from [`Self::incremental_cursor`], so multiple concurrent
1375/// downloads at the same urgency interleave their DATA frames fairly.
1376///
1377/// Streams without an explicit `priority` header get the RFC 9218 defaults:
1378/// urgency 3, incremental false.
1379#[derive(Default)]
1380pub struct Prioriser {
1381    /// Per-stream priority: stream_id -> (urgency 0-7, incremental flag)
1382    priorities: HashMap<StreamId, (u8, bool)>,
1383    /// RFC 9218 §4 round-robin cursor: stream ID that fired first in the
1384    /// last write pass over the incremental tail of the lowest-urgency
1385    /// bucket that contained at least one incremental stream. The next pass
1386    /// starts from the stream immediately after this ID (wrapping around),
1387    /// so a single slow-draining stream cannot hog the connection.
1388    ///
1389    /// `0` is the "no cursor yet" sentinel and means "start from the
1390    /// smallest ID in the bucket" — H2 stream IDs are always > 0.
1391    incremental_cursor: StreamId,
1392}
1393
1394/// RFC 9218 §4 default urgency value.
1395const DEFAULT_URGENCY: u8 = 3;
1396
1397/// Maximum entries in the priority map to prevent flooding via PRIORITY frames.
1398const MAX_PRIORITIES: usize = 4096;
1399
1400/// Small look-ahead window (in stream IDs) for PRIORITY frames that arrive
1401/// slightly before the peer opens the corresponding stream. RFC 9218 allows
1402/// PRIORITY to be sent for an idle stream that the peer intends to open
1403/// soon. Past this budget we assume the ID will never be used and drop the
1404/// entry, preventing flooding with far-future stream IDs.
1405const PRIORITY_IDLE_LOOKAHEAD: u32 = 64;
1406
1407impl Prioriser {
1408    /// Record or update the priority for a stream that we know exists or are
1409    /// currently processing (used from pkawa's header-handling path where the
1410    /// owning stream's HEADERS frame is being decoded).
1411    ///
1412    /// Returns `true` if the priority is invalid (self-dependency for RFC 7540),
1413    /// signalling the caller should reset the stream with a protocol error.
1414    pub fn push_priority(&mut self, stream_id: StreamId, priority: parser::PriorityPart) -> bool {
1415        trace!(
1416            "{} PRIORITY REQUEST FOR {}: {:?}",
1417            log_module_context!(),
1418            stream_id,
1419            priority
1420        );
1421        // Pre-condition: the priority map never grows past MAX_PRIORITIES.
1422        // The cap is the only thing standing between a PRIORITY flood and
1423        // unbounded memory; assert it holds on entry (each insert path below
1424        // either updates an existing key or is gated by this check).
1425        debug_assert!(
1426            self.priorities.len() <= MAX_PRIORITIES,
1427            "priority map must never exceed MAX_PRIORITIES entries"
1428        );
1429        // Cap the priority map to prevent flooding via PRIORITY frames
1430        if !self.priorities.contains_key(&stream_id) && self.priorities.len() >= MAX_PRIORITIES {
1431            return false;
1432        }
1433        match priority {
1434            parser::PriorityPart::Rfc7540 {
1435                stream_dependency,
1436                weight: _,
1437            } => {
1438                // RFC 9113 §5.3.1: a stream cannot depend on itself; signal
1439                // the caller to RST_STREAM with PROTOCOL_ERROR. Otherwise the
1440                // RFC 7540 priority tree is deprecated and silently ignored.
1441                stream_dependency.stream_id == stream_id
1442            }
1443            parser::PriorityPart::Rfc9218 {
1444                urgency,
1445                incremental,
1446            } => {
1447                // RFC 9218 §7.1: a malformed or out-of-range priority field
1448                // MUST be "treated as absent", NOT as a stream error. Clamping
1449                // an urgency > 7 to 7 is the policy-correct interpretation:
1450                // the field is still present (so defaulting would lose
1451                // information) but its value is normalised to the RFC's
1452                // allowed range [0..=7]. Intentionally not PROTOCOL_ERROR.
1453                self.priorities
1454                    .insert(stream_id, (urgency.min(7), incremental));
1455                // Post-conditions: the entry now exists with a clamped urgency
1456                // in [0, 7] (the writable scheduler buckets by urgency and would
1457                // mis-order on a value above 7), and the map stays within its
1458                // memory cap.
1459                debug_assert!(
1460                    self.priorities
1461                        .get(&stream_id)
1462                        .is_some_and(|(u, _)| *u <= 7),
1463                    "stored RFC 9218 urgency must be clamped to [0, 7]"
1464                );
1465                debug_assert!(
1466                    self.priorities.len() <= MAX_PRIORITIES,
1467                    "priority map must stay within MAX_PRIORITIES after insert"
1468                );
1469                false
1470            }
1471        }
1472    }
1473
1474    /// Record or update the priority for a stream ID that arrived via a
1475    /// standalone PRIORITY frame.
1476    ///
1477    /// Pass 3 Medium #4: without this guard, a peer could send PRIORITY for
1478    /// arbitrary stream IDs (e.g. 2^31 ever-increasing IDs) and pin up to
1479    /// `MAX_PRIORITIES` entries of memory. Accept only:
1480    /// - an ID that corresponds to a currently-open stream (`open_streams`);
1481    /// - an idle ID slightly ahead of `last_stream_id` (within
1482    ///   [`PRIORITY_IDLE_LOOKAHEAD`]), matching RFC 9218's "set priority for
1483    ///   a stream about to be opened" pattern.
1484    ///
1485    /// IDs in the past that we do not currently track (already closed) and
1486    /// IDs too far in the future are silently dropped. The `MAX_PRIORITIES`
1487    /// ceiling is preserved as a defensive backstop if both filters are ever
1488    /// circumvented.
1489    ///
1490    /// Returns the same value semantics as [`Self::push_priority`].
1491    pub fn push_priority_guarded(
1492        &mut self,
1493        stream_id: StreamId,
1494        priority: parser::PriorityPart,
1495        last_stream_id: StreamId,
1496        open_streams: &HashMap<StreamId, GlobalStreamId>,
1497    ) -> bool {
1498        if !self.is_acceptable(stream_id, last_stream_id, open_streams) {
1499            trace!(
1500                "{} PRIORITY dropped for unknown/far stream {} (last_stream_id={})",
1501                log_module_context!(),
1502                stream_id,
1503                last_stream_id
1504            );
1505            return false;
1506        }
1507        self.push_priority(stream_id, priority)
1508    }
1509
1510    fn is_acceptable(
1511        &self,
1512        stream_id: StreamId,
1513        last_stream_id: StreamId,
1514        open_streams: &HashMap<StreamId, GlobalStreamId>,
1515    ) -> bool {
1516        if open_streams.contains_key(&stream_id) {
1517            return true;
1518        }
1519        // Idle stream ahead of the current counter: accept a small look-ahead.
1520        // Past IDs that are NOT in `open_streams` are closed — drop them.
1521        let upper = last_stream_id.saturating_add(PRIORITY_IDLE_LOOKAHEAD);
1522        stream_id > last_stream_id && stream_id <= upper
1523    }
1524
1525    /// Remove a stream's priority entry (called when the stream is recycled).
1526    pub fn remove(&mut self, stream_id: &StreamId) {
1527        let had = self.priorities.contains_key(stream_id);
1528        let before = self.priorities.len();
1529        self.priorities.remove(stream_id);
1530        // Post-conditions: the entry is truly gone, and the map shrinks by
1531        // exactly one iff it was present. A leak here re-introduces the
1532        // PRIORITY-flood memory exposure the cap defends against.
1533        debug_assert!(
1534            !self.priorities.contains_key(stream_id),
1535            "remove must evict the priority entry"
1536        );
1537        debug_assert_eq!(
1538            self.priorities.len(),
1539            before - had as usize,
1540            "priority map length drops by exactly one iff the id was present"
1541        );
1542    }
1543
1544    /// Look up the priority for a stream, returning RFC 9218 defaults if absent.
1545    #[inline]
1546    pub fn get(&self, stream_id: &StreamId) -> (u8, bool) {
1547        self.priorities
1548            .get(stream_id)
1549            .copied()
1550            .unwrap_or((DEFAULT_URGENCY, false))
1551    }
1552
1553    /// Reorder a pre-sorted slice of writable stream IDs so that inside each
1554    /// urgency bucket, incremental streams appear after non-incremental ones,
1555    /// and the incremental tail is rotated by [`Self::incremental_cursor`]
1556    /// (RFC 9218 §4).
1557    ///
1558    /// The input `buf` must already be sorted by `(urgency, stream_id)`:
1559    /// this routine only partitions and rotates inside same-urgency
1560    /// contiguous runs, it does not re-sort.
1561    ///
1562    /// Returns the total number of incremental streams seen, so callers that
1563    /// need to update the cursor at the end of the write pass can early-exit
1564    /// when the count is zero.
1565    pub fn apply_incremental_rotation(&self, buf: &mut [StreamId]) -> usize {
1566        // Pre-condition: callers must hand a slice already sorted by urgency so
1567        // same-urgency runs are contiguous (this routine only partitions/rotates
1568        // within a run, it does not re-sort across urgencies). A non-monotonic
1569        // urgency sequence would split one logical bucket into several and
1570        // mis-schedule the round-robin. `windows(2)` over a slice of size N is
1571        // dead code in release.
1572        #[cfg(debug_assertions)]
1573        debug_assert!(
1574            buf.windows(2)
1575                .all(|w| self.get(&w[0]).0 <= self.get(&w[1]).0),
1576            "apply_incremental_rotation requires input pre-sorted by urgency"
1577        );
1578        let len_before = buf.len();
1579        #[cfg(debug_assertions)]
1580        let expected_incremental = buf.iter().filter(|id| self.get(id).1).count();
1581        let mut total_incremental = 0usize;
1582        let mut i = 0;
1583        while i < buf.len() {
1584            let (urgency_i, _) = self.get(&buf[i]);
1585            let mut j = i + 1;
1586            while j < buf.len() {
1587                let (urgency_j, _) = self.get(&buf[j]);
1588                if urgency_j != urgency_i {
1589                    break;
1590                }
1591                j += 1;
1592            }
1593            // `buf[i..j]` is a contiguous run of same-urgency stream IDs.
1594            let bucket = &mut buf[i..j];
1595            if bucket.len() > 1 {
1596                // Stable partition: non-incremental first, incremental last,
1597                // each subrange staying in ascending stream-id order.
1598                bucket.sort_by_key(|id| self.get(id).1);
1599                let split = bucket.partition_point(|id| !self.get(id).1);
1600                let incremental_tail = &mut bucket[split..];
1601                if incremental_tail.len() > 1 {
1602                    // Rotate so the pass starts right after the stream that
1603                    // fired first previously. `partition_point` returns the
1604                    // first index whose stream ID > cursor (so cursor itself
1605                    // is still drained, but after the streams ahead of it).
1606                    let start =
1607                        incremental_tail.partition_point(|id| *id <= self.incremental_cursor);
1608                    incremental_tail.rotate_left(start);
1609                }
1610                total_incremental += incremental_tail.len();
1611            } else if bucket.len() == 1 && self.get(&bucket[0]).1 {
1612                total_incremental += 1;
1613            }
1614            i = j;
1615        }
1616        // Post-conditions: the routine is a permutation — it reorders in place
1617        // and never drops a stream id (len unchanged), and the returned count is
1618        // exactly the number of incremental streams present (the cursor-advance
1619        // callers rely on this being the true incremental-tail size).
1620        debug_assert_eq!(
1621            buf.len(),
1622            len_before,
1623            "rotation must preserve the slice (no streams dropped or added)"
1624        );
1625        #[cfg(debug_assertions)]
1626        debug_assert_eq!(
1627            total_incremental, expected_incremental,
1628            "reported incremental count must equal the incremental streams in buf"
1629        );
1630        total_incremental
1631    }
1632
1633    /// Advance the RFC 9218 §4 round-robin cursor after a write pass.
1634    ///
1635    /// `first_incremental_fired` is the stream ID that headed the incremental
1636    /// tail we just drained; the next pass will start at the next stream
1637    /// after that ID. Callers may pass `None` when no incremental streams
1638    /// were eligible, leaving the cursor where it was.
1639    pub fn advance_incremental_cursor(&mut self, first_incremental_fired: Option<StreamId>) {
1640        if let Some(id) = first_incremental_fired {
1641            self.incremental_cursor = id;
1642        }
1643    }
1644}
1645
1646/// Connection-level flow control state (RFC 9113 §6.9).
1647pub struct H2FlowControl {
1648    /// Connection-level send window (can go negative per RFC 9113 §6.9.2).
1649    pub window: i32,
1650    /// Bytes received since last connection-level WINDOW_UPDATE.
1651    pub received_bytes_since_update: u32,
1652    /// Queued stream_id -> accumulated increment for WINDOW_UPDATE frames (O(1) coalescing).
1653    pub pending_window_updates: HashMap<u32, u32>,
1654}
1655
1656/// Byte accounting for connection overhead attribution.
1657pub struct H2ByteAccounting {
1658    /// Bytes read on the zero stream not yet attributed to a stream.
1659    pub zero_bytes_read: usize,
1660    /// Overhead bytes received (connection-level frames).
1661    pub overhead_bin: usize,
1662    /// Overhead bytes sent (connection-level frames).
1663    pub overhead_bout: usize,
1664}
1665
1666/// Connection draining state for graceful shutdown.
1667pub struct H2DrainState {
1668    /// True when we've sent GOAWAY and are draining.
1669    pub draining: bool,
1670    /// Last stream ID from peer's GOAWAY (for retry decisions).
1671    pub peer_last_stream_id: Option<StreamId>,
1672    /// Wall-clock timestamp captured the first time this connection entered
1673    /// `draining` during soft-stop. Used together with
1674    /// [`Self::graceful_shutdown_deadline`] to decide when to force-close.
1675    /// Remains `None` until the proxy-initiated drain begins (peer-initiated
1676    /// drains via `handle_goaway_frame` don't arm the forced-close timer —
1677    /// the caller in `Mux::shutting_down` is the only writer).
1678    pub started_at: Option<Instant>,
1679    /// Wall-clock budget granted to in-flight streams after the initial
1680    /// `GOAWAY(NO_ERROR)`. `None` means "wait indefinitely" (knob value `0`).
1681    /// Default when unset upstream: 5 s (see `L7ListenerHandler`).
1682    pub graceful_shutdown_deadline: Option<std::time::Duration>,
1683}
1684
1685pub struct ConnectionH2<Front: SocketHandler> {
1686    /// Connection/session ULID propagated from the parent [`Mux`]. Used to
1687    /// stamp the session slot of the `[session req cluster backend]` log
1688    /// prefix emitted by this module's `log_context!` / `log_context_stream!`
1689    /// macros.
1690    pub session_ulid: Ulid,
1691    pub decoder: loona_hpack::Decoder<'static>,
1692    pub encoder: loona_hpack::Encoder<'static>,
1693    pub expect_read: Option<(H2StreamId, usize)>,
1694    pub expect_write: Option<H2StreamId>,
1695    pub last_stream_id: StreamId,
1696    pub local_settings: H2Settings,
1697    pub peer_settings: H2Settings,
1698    pub position: Position,
1699    pub prioriser: Prioriser,
1700    pub readiness: Readiness,
1701    pub socket: Front,
1702    pub state: H2State,
1703    pub streams: HashMap<StreamId, GlobalStreamId>,
1704    pub timeout_container: TimeoutContainer,
1705    /// Connection-level flow control state (send window, receive tracking, pending updates).
1706    pub flow_control: H2FlowControl,
1707    /// Highest stream ID accepted from the peer (used for GoAway last_stream_id).
1708    pub highest_peer_stream_id: StreamId,
1709    /// RFC 7541 §4.2 / §6.3 pending dynamic-table-size-update signal.
1710    ///
1711    /// `Some(new_size)` when a peer SETTINGS frame adjusted
1712    /// `SETTINGS_HEADER_TABLE_SIZE` and we have not yet prepended the
1713    /// matching `001xxxxx` HPACK directive to a header block. Consumed and
1714    /// cleared by [`H2BlockConverter::emit_pending_size_update_if_new_block`]
1715    /// on the next `Block::StatusLine` or `Block::Header` encoded for the
1716    /// connection. Until then the peer's decoder still has its previous
1717    /// (possibly larger) table cap, so emitting is a correctness
1718    /// requirement, not a nicety — see the RFC 9113 encoder-decoder
1719    /// synchronisation contract (§6.5.2).
1720    pub pending_table_size_update: Option<u32>,
1721    /// Reusable buffer for HPACK-encoded headers in the H2 block converter.
1722    pub converter_buf: Vec<u8>,
1723    /// Reusable buffer for lowercasing header keys in the H2 block converter.
1724    pub lowercase_buf: Vec<u8>,
1725    /// Reusable buffer for assembling cookie values in the H2 block converter.
1726    pub cookie_buf: Vec<u8>,
1727    /// Connection draining state for graceful shutdown.
1728    pub drain: H2DrainState,
1729    pub zero: GenericHttpStream,
1730    /// Byte accounting for connection overhead attribution.
1731    pub bytes: H2ByteAccounting,
1732    /// Flood detector for CVE mitigations (Rapid Reset, CONTINUATION, Ping, Settings floods).
1733    pub flood_detector: H2FloodDetector,
1734    /// RFC 9113 §6.5: timestamp when we sent SETTINGS and are awaiting ACK.
1735    /// If the peer does not ACK within SETTINGS_ACK_TIMEOUT, we send GOAWAY
1736    /// with SettingsTimeout error.
1737    pub settings_sent_at: Option<Instant>,
1738    /// Queued RST_STREAM frames to send: Vec<(stream_id, error_code)>.
1739    /// Used when refusing streams (MAX_CONCURRENT_STREAMS, buffer exhaustion)
1740    /// during readable — the actual write happens in the writable preamble
1741    /// to avoid conflicting with kawa.storage usage for frame payload discard.
1742    pub pending_rst_streams: Vec<(StreamId, H2Error)>,
1743    /// RFC 9113 §6.8: tracks stream IDs for which RST_STREAM has already been sent,
1744    /// preventing duplicate RST_STREAM frames on the wire.
1745    pub rst_sent: HashSet<StreamId>,
1746    /// Lifetime counter of RST_STREAM frames queued (pending + already flushed).
1747    /// Used to detect sustained misbehavior even when writable() drains the
1748    /// pending queue between readable() calls.
1749    pub total_rst_streams_queued: usize,
1750    /// Reusable buffer for priority-sorted stream IDs in write_streams().
1751    /// Cleared and reused each call to avoid per-frame allocation.
1752    priorities_buf: Vec<StreamId>,
1753    /// True once we've asked rustls to emit TLS close_notify for this frontend.
1754    close_notify_sent: bool,
1755    /// Per-listener H2 connection tuning (window size, max streams, shrink ratio).
1756    pub connection_config: H2ConnectionConfig,
1757    /// Maximum pending WINDOW_UPDATE entries before dropping.
1758    /// Derived from `connection_config.max_concurrent_streams` at construction.
1759    max_pending_window_updates: usize,
1760    /// Last `(connection_window, active_streams, pending_window_updates)` snapshot
1761    /// emitted by [`Self::gauge_connection_state`]. The snapshot represents this
1762    /// connection's *contribution* to the three `h2.connection.*` aggregate
1763    /// gauges; each call emits the signed delta against this snapshot via
1764    /// [`gauge_add!`] so the gauge sums across connections.
1765    ///
1766    /// Stays `None` until the first emission. [`Drop`] applies the negative of
1767    /// this snapshot so the connection's contribution is always rebalanced to
1768    /// zero on teardown — independent of which close path runs.
1769    last_gauge_snapshot: Option<(usize, usize, usize)>,
1770    /// Per-stream wall-clock timestamp of last meaningful activity (DATA or
1771    /// HEADERS frame receipt). Used to cancel streams that make no forward
1772    /// progress within [`Self::stream_idle_timeout`] — mitigates slow-multiplex
1773    /// Slowloris: connection-level idle timers reset on every frame, so a
1774    /// misbehaving peer can otherwise pin up to `max_concurrent_streams` slots
1775    /// for the full nominal connection timeout.
1776    ///
1777    /// Initialized when the stream is created and refreshed on each non-empty
1778    /// inbound DATA frame and on HEADERS for an existing stream (trailers).
1779    /// Empty DATA frames (CVE-2019-9518 vector) do NOT refresh the timer.
1780    pub stream_last_activity_at: HashMap<StreamId, Instant>,
1781    /// Per-stream timestamp of when the stream first became flow-control-stalled
1782    /// on the OUTBOUND (response) side — it holds buffered response data it
1783    /// cannot drain because its effective send window `min(stream.window,
1784    /// connection.window)` is exhausted (the HTTP/2 window-stall /
1785    /// WINDOW_UPDATE-drip vector). Distinct from [`Self::stream_last_activity_at`]:
1786    /// this map is armed/cleared ONLY by outbound flow-control progress and is
1787    /// NEVER refreshed by inbound DATA/HEADERS or connection-level frames, so a
1788    /// peer dribbling 1-byte DATA on a stalled stream cannot keep it warm (the
1789    /// liveness timer alone misses this because inbound drips refresh it). Reaped
1790    /// by [`Self::cancel_timed_out_streams`] after [`Self::stream_idle_timeout`].
1791    pub stream_fc_stalled_since: HashMap<StreamId, Instant>,
1792    /// Cumulative outbound flow-control bytes drained on a window-stalled stream
1793    /// SINCE its [`Self::stream_fc_stalled_since`] deadline was armed (M2
1794    /// cumulative-stall budget). An entry exists IFF `stream_fc_stalled_since`
1795    /// has one for the stream; the two maps are kept in lockstep at every
1796    /// arm/clear/evict site. Closes the `WINDOW_UPDATE(+1)`-drip residual: a
1797    /// 1-byte drain no longer clears the deadline — only cumulative progress
1798    /// reaching [`FC_STALL_CLEAR_FLOOR`] does.
1799    pub stream_fc_stalled_progress: HashMap<StreamId, usize>,
1800    /// Per-stream idle cap. Streams with no activity for longer than this are
1801    /// RST_STREAM(CANCEL)'d by [`Self::cancel_timed_out_streams`].
1802    pub stream_idle_timeout: std::time::Duration,
1803    /// RFC 9113 §5.1.2 back-pressure: count of stream refusals
1804    /// (REFUSED_STREAM emitted via [`Self::refuse_stream_and_discard`]) within
1805    /// the current back-pressure window. When the count exceeds
1806    /// [`BACKPRESSURE_REFUSAL_THRESHOLD`] inside one
1807    /// [`BACKPRESSURE_WINDOW_DURATION`] we halve the advertised
1808    /// `SETTINGS_MAX_CONCURRENT_STREAMS` to signal the peer to slow down.
1809    refuse_count_window: u32,
1810    /// Start timestamp for the current back-pressure window.
1811    refuse_window_start: Instant,
1812    /// Set once we have halved `local_settings.settings_max_concurrent_streams`
1813    /// in response to a refusal burst. Prevents the cap from collapsing to 0
1814    /// on sustained abuse — a single halving per connection is sufficient to
1815    /// signal back-pressure; further bursts trigger `EnhanceYourCalm`.
1816    mcs_backpressure_applied: bool,
1817}
1818impl<Front: SocketHandler> std::fmt::Debug for ConnectionH2<Front> {
1819    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1820        f.debug_struct("ConnectionH2")
1821            .field("position", &self.position)
1822            .field("state", &self.state)
1823            .field("expect", &self.expect_read)
1824            .field("readiness", &self.readiness)
1825            .field("local_settings", &self.local_settings)
1826            .field("peer_settings", &self.peer_settings)
1827            .field("socket", &self.socket.socket_ref())
1828            .field("streams", &self.streams)
1829            .field("zero", &self.zero.storage.meter(20))
1830            .field("window", &self.flow_control.window)
1831            .field("total_rst_streams_queued", &self.total_rst_streams_queued)
1832            .finish()
1833    }
1834}
1835
1836/// Symmetric tear-down for the three `h2.connection.*` aggregate gauges:
1837/// whatever positive contribution this connection made via
1838/// [`ConnectionH2::gauge_connection_state`] is subtracted back out when the
1839/// connection is dropped.
1840///
1841/// Using `Drop` (rather than wiring decrements into every close path —
1842/// `graceful_goaway`, `force_disconnect`, `handle_goaway_frame`, `Mux::close`,
1843/// stream-id exhaustion, panic-unwind) is what guarantees the gauge is
1844/// arithmetically symmetric regardless of which path teardown took. Past
1845/// underflow incidents (commits a650ad69, d2f01ed4) have all been
1846/// missing-decrement bugs that `Drop` makes structurally impossible.
1847impl<Front: SocketHandler> Drop for ConnectionH2<Front> {
1848    fn drop(&mut self) {
1849        self.release_connection_gauges();
1850    }
1851}
1852
1853#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1854pub enum H2StreamId {
1855    Zero,
1856    Other { id: StreamId, gid: GlobalStreamId },
1857}
1858
1859impl<Front: SocketHandler> ConnectionH2<Front> {
1860    fn frontend_hung_up_while_draining(&self) -> bool {
1861        matches!(self.position, Position::Server)
1862            && self.drain.draining
1863            && (self.readiness.event.is_hup() || self.readiness.event.is_error())
1864    }
1865
1866    /// Once the final GOAWAY has been queued and all streams/control frames are
1867    /// gone, a peer-side HUP/ERR means any remaining rustls backlog is no
1868    /// longer deliverable. Waiting on `socket_wants_write()` in that state can
1869    /// deadlock shutdown forever because GOAWAY disables further frame reads.
1870    fn peer_gone_after_final_goaway(&self) -> bool {
1871        self.frontend_hung_up_while_draining()
1872            && matches!(self.state, H2State::GoAway | H2State::Error)
1873            && self.streams.is_empty()
1874            && self.expect_write.is_none()
1875            && self.zero.storage.is_empty()
1876    }
1877
1878    /// Shared constructor for both server and client H2 connections.
1879    ///
1880    /// Differences between server and client are captured by the caller-provided
1881    /// `position`, `expect_read`, and `readiness_interest` parameters.
1882    #[allow(clippy::too_many_arguments)]
1883    pub(super) fn new(
1884        session_ulid: Ulid,
1885        socket: Front,
1886        position: super::Position,
1887        pool: std::rc::Weak<std::cell::RefCell<crate::pool::Pool>>,
1888        flood_config: H2FloodConfig,
1889        connection_config: H2ConnectionConfig,
1890        stream_idle_timeout: std::time::Duration,
1891        graceful_shutdown_deadline: Option<std::time::Duration>,
1892        timeout_container: crate::timer::TimeoutContainer,
1893        expect_read: Option<(H2StreamId, usize)>,
1894        readiness_interest: sozu_command::ready::Ready,
1895    ) -> Option<Self> {
1896        let buffer = pool
1897            .upgrade()
1898            .and_then(|pool| pool.borrow_mut().checkout())?;
1899        let local_settings = H2Settings {
1900            settings_max_concurrent_streams: connection_config.max_concurrent_streams,
1901            ..H2Settings::default()
1902        };
1903        let mut decoder = loona_hpack::Decoder::new();
1904        // RFC 7541 §4.2: enforce SETTINGS_HEADER_TABLE_SIZE as the upper bound
1905        // for dynamic table size updates from the peer
1906        decoder.set_max_allowed_table_size(local_settings.settings_header_table_size as usize);
1907        Some(ConnectionH2 {
1908            session_ulid,
1909            decoder,
1910            encoder: loona_hpack::Encoder::new(),
1911            expect_read,
1912            expect_write: None,
1913            last_stream_id: 0,
1914            local_settings,
1915            peer_settings: H2Settings::default(),
1916            position,
1917            prioriser: Prioriser::default(),
1918            readiness: crate::Readiness {
1919                interest: readiness_interest,
1920                event: Ready::EMPTY,
1921            },
1922            socket,
1923            state: H2State::ClientPreface,
1924            streams: std::collections::HashMap::with_capacity(8),
1925            timeout_container,
1926            flow_control: H2FlowControl {
1927                window: DEFAULT_INITIAL_WINDOW_SIZE as i32,
1928                received_bytes_since_update: 0,
1929                pending_window_updates: HashMap::new(),
1930            },
1931            highest_peer_stream_id: 0,
1932            pending_table_size_update: None,
1933            converter_buf: Vec::new(),
1934            lowercase_buf: Vec::new(),
1935            cookie_buf: Vec::new(),
1936            drain: H2DrainState {
1937                draining: false,
1938                peer_last_stream_id: None,
1939                started_at: None,
1940                graceful_shutdown_deadline,
1941            },
1942            zero: kawa::Kawa::new(kawa::Kind::Request, kawa::Buffer::new(buffer)),
1943            bytes: H2ByteAccounting {
1944                zero_bytes_read: 0,
1945                overhead_bin: 0,
1946                overhead_bout: 0,
1947            },
1948            flood_detector: H2FloodDetector::new(flood_config),
1949            settings_sent_at: None,
1950            pending_rst_streams: Vec::new(),
1951            rst_sent: std::collections::HashSet::new(),
1952            total_rst_streams_queued: 0,
1953            priorities_buf: Vec::new(),
1954            close_notify_sent: false,
1955            max_pending_window_updates: 1 + connection_config.max_concurrent_streams as usize * 4,
1956            connection_config,
1957            last_gauge_snapshot: None,
1958            stream_last_activity_at: HashMap::new(),
1959            stream_fc_stalled_since: HashMap::new(),
1960            stream_fc_stalled_progress: HashMap::new(),
1961            stream_idle_timeout,
1962            refuse_count_window: 0,
1963            refuse_window_start: Instant::now(),
1964            mcs_backpressure_applied: false,
1965        })
1966    }
1967
1968    /// Start TLS close_notify on the frontend and keep the session alive until
1969    /// rustls has flushed the generated records.
1970    pub fn initiate_close_notify(&mut self) -> bool {
1971        if !self.position.is_server()
1972            || matches!(
1973                self.state,
1974                H2State::ClientPreface | H2State::ClientSettings | H2State::ServerSettings
1975            )
1976        {
1977            return false;
1978        }
1979        if !self.close_notify_sent {
1980            trace!("{} H2 initiating CLOSE_NOTIFY", log_context!(self));
1981            self.socket.socket_close();
1982            self.close_notify_sent = true;
1983        }
1984        if self.socket.socket_wants_write() {
1985            self.readiness.interest = Ready::WRITABLE | Ready::HUP | Ready::ERROR;
1986            self.ensure_tls_flushed();
1987            true
1988        } else {
1989            false
1990        }
1991    }
1992
1993    fn expect_header(&mut self) {
1994        self.state = H2State::Header;
1995        self.expect_read = Some((H2StreamId::Zero, 9));
1996    }
1997
1998    /// Process the `H2State::Header` state: parse a 9-byte frame header from
1999    /// `self.zero`, validate the stream, create new streams if needed, and
2000    /// transition to `H2State::Frame` for the payload.
2001    ///
2002    /// Returns `MuxResult` — the caller should propagate the result directly.
2003    fn handle_header_state<L>(&mut self, context: &mut Context<L>) -> MuxResult
2004    where
2005        L: ListenerHandler + L7ListenerHandler,
2006    {
2007        let i = self.zero.storage.data();
2008        trace!("{}   header: {:?}", log_context!(self), i);
2009        match parser::frame_header(i, self.local_settings.settings_max_frame_size) {
2010            Ok((_, header)) => {
2011                trace!("{} {:#?}", log_context!(self), header);
2012                self.zero.storage.clear();
2013                let stream_id = header.stream_id;
2014                // RFC 9113 §6.10: CONTINUATION frames MUST be preceded by a
2015                // HEADERS or PUSH_PROMISE frame without END_HEADERS. When we
2016                // reach `handle_header_state`, we are between frames and no
2017                // header block is in progress (otherwise the state would be
2018                // `H2State::ContinuationHeader`). A CONTINUATION frame arriving
2019                // here is therefore standalone and MUST be treated as a
2020                // connection error of type PROTOCOL_ERROR.
2021                if header.frame_type == FrameType::Continuation {
2022                    error!(
2023                        "{} standalone CONTINUATION frame on stream {} without preceding HEADERS",
2024                        log_context!(self),
2025                        stream_id
2026                    );
2027                    return self.goaway(H2Error::ProtocolError);
2028                }
2029                // RFC 9113 §5.5: unknown frame types MUST be ignored and discarded.
2030                // Route unknown frames (and any stream_id == 0 control frame)
2031                // through stream 0 (the connection-level buffer) so
2032                // `handle_frame` can drop them without touching stream state.
2033                let read_stream = if stream_id == 0
2034                    || matches!(header.frame_type, FrameType::Unknown(_))
2035                {
2036                    H2StreamId::Zero
2037                } else if let Some(global_stream_id) = self.streams.get(&stream_id) {
2038                    let allowed_on_half_closed = header.frame_type == FrameType::WindowUpdate
2039                        || header.frame_type == FrameType::Priority
2040                        || header.frame_type == FrameType::RstStream;
2041                    let stream = &context.streams[*global_stream_id];
2042                    // Use the position-aware end_of_stream flag:
2043                    // - Server reads from front (client requests)
2044                    // - Client reads from back (backend responses)
2045                    let received_eos = if self.position.is_server() {
2046                        stream.front_received_end_of_stream
2047                    } else {
2048                        stream.back_received_end_of_stream
2049                    };
2050                    trace!(
2051                        "{} REQUESTING EXISTING STREAM {}: {}/{:?}",
2052                        log_context!(self),
2053                        stream_id,
2054                        received_eos,
2055                        stream.state
2056                    );
2057                    if !allowed_on_half_closed && (received_eos || !stream.state.is_open()) {
2058                        error!(
2059                            "{} CANNOT RECEIVE {:?} ON THIS STREAM {:?}",
2060                            log_context!(self),
2061                            header.frame_type,
2062                            stream.state
2063                        );
2064                        return self.goaway(H2Error::StreamClosed);
2065                    }
2066                    // RFC 9113 §8.1: a HEADERS frame received in the body
2067                    // phase is a trailer block and MUST carry END_STREAM. This
2068                    // closes the request-smuggling primitive where a peer sends
2069                    // HEADERS, DATA, HEADERS (no END_STREAM) to chain header
2070                    // blocks on the same stream ID.
2071                    //
2072                    // Discriminate from the read-side Kawa parsing phase rather
2073                    // than stream existence: on Position::Client the stream is
2074                    // created when we send the request to the backend, so the
2075                    // initial backend response HEADERS legitimately arrives on
2076                    // an existing stream. Similarly, 1xx→final transitions on
2077                    // either side may yield multiple HEADERS frames before the
2078                    // body begins (kawa clears back to initial / terminated on
2079                    // 1xx; neither is main_phase). Only HEADERS arriving once
2080                    // the read side has transitioned to Body/Chunks parsing —
2081                    // i.e. after headers were fully consumed and body framing
2082                    // is in progress — may be a trailer.
2083                    let read_in_body = if self.position.is_server() {
2084                        stream.front.is_main_phase()
2085                    } else {
2086                        stream.back.is_main_phase()
2087                    };
2088                    if header.frame_type == FrameType::Headers
2089                        && read_in_body
2090                        && header.flags & parser::FLAG_END_STREAM == 0
2091                    {
2092                        error!(
2093                            "{} HEADERS without END_STREAM on open stream {} in body phase: trailers MUST carry END_STREAM",
2094                            log_context!(self),
2095                            stream_id
2096                        );
2097                        return self.goaway(H2Error::ProtocolError);
2098                    }
2099                    if header.frame_type == FrameType::Data {
2100                        H2StreamId::Other {
2101                            id: stream_id,
2102                            gid: *global_stream_id,
2103                        }
2104                    } else {
2105                        H2StreamId::Zero
2106                    }
2107                } else {
2108                    // RFC 9113 §5.1.1: stream identifiers MUST be strictly
2109                    // increasing. Tightened from `>=` to `>` so that a peer
2110                    // cannot re-use `self.last_stream_id` (which would
2111                    // conflict with our own server-pushed streams if we
2112                    // ever enable push in the future). For the first
2113                    // request on a fresh connection `last_stream_id == 0`
2114                    // and any client-initiated odd stream still passes.
2115                    if header.frame_type == FrameType::Headers
2116                        && self.position.is_server()
2117                        && stream_id & 1 == 1
2118                        && stream_id > self.last_stream_id
2119                    {
2120                        // RFC 9113 §6.8: after sending a GOAWAY, the proxy
2121                        // MUST NOT accept new streams.
2122                        // `graceful_goaway` sets `drain.draining = true`
2123                        // and sends an initial GOAWAY with last_stream_id =
2124                        // STREAM_ID_MAX (so in-flight requests are still
2125                        // accepted), but the contract for *new* peer-
2126                        // initiated streams is that they must be refused.
2127                        // Without this check, a peer racing the drain
2128                        // window could open arbitrary new streams between
2129                        // the initial and final GOAWAY emission.
2130                        if self.drain.draining {
2131                            if stream_id > self.highest_peer_stream_id {
2132                                self.highest_peer_stream_id = stream_id;
2133                            }
2134                            return self.refuse_stream_and_discard(
2135                                stream_id,
2136                                H2Error::RefusedStream,
2137                                header.payload_len,
2138                            );
2139                        }
2140                        if self.streams.len()
2141                            >= self.local_settings.settings_max_concurrent_streams as usize
2142                        {
2143                            error!(
2144                                "{} MAX CONCURRENT STREAMS: limit={}, current={}",
2145                                log_context!(self),
2146                                self.local_settings.settings_max_concurrent_streams,
2147                                self.streams.len()
2148                            );
2149                            // RFC 9113 §6.8: update highest_peer_stream_id BEFORE
2150                            // queueing RST_STREAM so GOAWAY reports the correct
2151                            // last_stream_id if the connection closes later.
2152                            if stream_id > self.highest_peer_stream_id {
2153                                self.highest_peer_stream_id = stream_id;
2154                            }
2155                            return self.refuse_stream_and_discard(
2156                                stream_id,
2157                                H2Error::RefusedStream,
2158                                header.payload_len,
2159                            );
2160                        }
2161                        match self.create_stream(stream_id, context) {
2162                            Some(_) => {}
2163                            None => {
2164                                // Buffer pool exhaustion is transient — refuse
2165                                // this stream but keep the connection alive so
2166                                // existing streams can complete and free buffers.
2167                                error!(
2168                                    "{} Could not create stream {}: buffer pool exhausted",
2169                                    log_context!(self),
2170                                    stream_id
2171                                );
2172                                // RFC 9113 §6.8: update highest_peer_stream_id BEFORE
2173                                // queueing RST_STREAM so GOAWAY reports the correct
2174                                // last_stream_id if the connection closes later.
2175                                if stream_id > self.highest_peer_stream_id {
2176                                    self.highest_peer_stream_id = stream_id;
2177                                }
2178                                return self.refuse_stream_and_discard(
2179                                    stream_id,
2180                                    H2Error::RefusedStream,
2181                                    header.payload_len,
2182                                );
2183                            }
2184                        }
2185                    } else if header.frame_type != FrameType::Priority {
2186                        // Distinguish closed vs idle: check whether the stream
2187                        // was previously opened. For Server position, compare
2188                        // against highest_peer_stream_id (client-initiated).
2189                        // For Client position, compare against last_stream_id
2190                        // (our own initiated streams) since the peer never
2191                        // initiates streams on a backend connection.
2192                        let is_closed_stream = if self.position.is_server() {
2193                            header.stream_id <= self.highest_peer_stream_id
2194                        } else {
2195                            header.stream_id < self.last_stream_id
2196                        };
2197                        if is_closed_stream {
2198                            match header.frame_type {
2199                                FrameType::RstStream | FrameType::WindowUpdate => {
2200                                    // RFC 9113 §5.1: RST_STREAM and WINDOW_UPDATE
2201                                    // on a closed stream can arrive due to race
2202                                    // conditions and should be consumed/discarded.
2203                                    debug!(
2204                                        "{} Ignoring {:?} on closed stream {}",
2205                                        log_context!(self),
2206                                        header.frame_type,
2207                                        header.stream_id
2208                                    );
2209                                    self.flood_detector.glitch_count += 1;
2210                                    check_flood_or_return!(self);
2211                                }
2212                                FrameType::Data => {
2213                                    // RFC 9113 §5.1: DATA on a closed stream is a
2214                                    // stream error of type STREAM_CLOSED. Queue
2215                                    // RST_STREAM (not GOAWAY) to preserve the
2216                                    // connection for other streams. The payload is
2217                                    // still routed through stream 0 so handle_frame
2218                                    // can do connection-level flow control accounting.
2219                                    debug!(
2220                                        "{} DATA on closed stream {}, sending RST_STREAM(STREAM_CLOSED)",
2221                                        log_context!(self),
2222                                        header.stream_id
2223                                    );
2224                                    self.flood_detector.glitch_count += 1;
2225                                    check_flood_or_return!(self);
2226                                    if let Some(result) =
2227                                        self.enqueue_rst(header.stream_id, H2Error::StreamClosed)
2228                                    {
2229                                        return result;
2230                                    }
2231                                }
2232                                _ => {
2233                                    // RFC 9113 §5.1: HEADERS or other frames on a
2234                                    // closed stream → connection error STREAM_CLOSED.
2235                                    error!(
2236                                        "{} Received {:?} on closed stream {}, sending GOAWAY(STREAM_CLOSED)",
2237                                        log_context!(self),
2238                                        header.frame_type,
2239                                        header.stream_id
2240                                    );
2241                                    return self.goaway(H2Error::StreamClosed);
2242                                }
2243                            }
2244                        } else {
2245                            error!(
2246                                "{} Received {:?} on idle stream {}, sending GOAWAY(PROTOCOL_ERROR)",
2247                                log_context!(self),
2248                                header.frame_type,
2249                                header.stream_id
2250                            );
2251                            return self.goaway(H2Error::ProtocolError);
2252                        }
2253                    }
2254                    H2StreamId::Zero
2255                };
2256                trace!(
2257                    "{} {} {:?} {:#?}",
2258                    log_context!(self),
2259                    header.stream_id,
2260                    stream_id,
2261                    self.streams
2262                );
2263                self.expect_read = Some((read_stream, header.payload_len as usize));
2264                self.state = H2State::Frame(header);
2265            }
2266            Err(error) => {
2267                let error = error_nom_to_h2(error);
2268                error!("{} COULD NOT PARSE FRAME HEADER", log_context!(self));
2269                return self.goaway(error);
2270            }
2271        };
2272        MuxResult::Continue
2273    }
2274
2275    /// Process the `H2State::ContinuationHeader` state: parse a CONTINUATION
2276    /// frame header from `self.zero`, validate stream ID continuity, track
2277    /// flood detection counters, and transition to `ContinuationFrame`.
2278    ///
2279    /// The `headers` parameter is the accumulated HEADERS context from the
2280    /// initial HEADERS frame (cloned from the state enum to avoid borrow
2281    /// conflicts).
2282    fn handle_continuation_header_state(&mut self, headers: &Headers) -> MuxResult {
2283        let i = self.zero.storage.unparsed_data();
2284        trace!("{}   continuation header: {:?}", log_context!(self), i);
2285        match parser::frame_header(i, self.local_settings.settings_max_frame_size) {
2286            Ok((
2287                _,
2288                FrameHeader {
2289                    payload_len,
2290                    frame_type: FrameType::Continuation,
2291                    flags,
2292                    stream_id,
2293                },
2294            )) => {
2295                if self.zero.storage.end < 9 {
2296                    error!(
2297                        "{} CONTINUATION header: storage.end ({}) too small to remove frame header",
2298                        log_context!(self),
2299                        self.zero.storage.end
2300                    );
2301                    return self.goaway(H2Error::InternalError);
2302                }
2303                self.zero.storage.end -= 9;
2304                if stream_id != headers.stream_id {
2305                    error!(
2306                        "{} CONTINUATION stream_id {} does not match HEADERS stream_id {}",
2307                        log_context!(self),
2308                        stream_id,
2309                        headers.stream_id
2310                    );
2311                    return self.goaway(H2Error::ProtocolError);
2312                }
2313                // CVE-2024-27316: track CONTINUATION frame count and accumulated size
2314                let cont_count_before = self.flood_detector.continuation_count;
2315                let acc_size_before = self.flood_detector.accumulated_header_size;
2316                self.flood_detector.continuation_count += 1;
2317                self.flood_detector.accumulated_header_size = self
2318                    .flood_detector
2319                    .accumulated_header_size
2320                    .saturating_add(payload_len);
2321                // Per-block CONTINUATION accounting must grow monotonically
2322                // within a header block: each frame bumps the count by one and
2323                // the accumulated size by the frame's payload (never shrinks
2324                // mid-block). `reset_continuation` is the only thing allowed to
2325                // zero these — and only once the block is complete.
2326                debug_assert_eq!(
2327                    self.flood_detector.continuation_count,
2328                    cont_count_before + 1,
2329                    "CONTINUATION per-block counter must advance by one per frame"
2330                );
2331                debug_assert!(
2332                    self.flood_detector.accumulated_header_size >= acc_size_before,
2333                    "accumulated header size must not shrink within a header block"
2334                );
2335                check_flood_or_return!(self);
2336                // RFC 9113 §10.5.1: reject header blocks that cannot be
2337                // buffered. Previously we silently removed READABLE interest
2338                // when amount > available_space, stalling the connection.
2339                // If the payload still fits in our zero buffer we can refuse
2340                // just this stream (RST_STREAM + drain); if not, the
2341                // connection can no longer decode header blocks safely and we
2342                // escalate to GOAWAY(EnhanceYourCalm).
2343                if self.flood_detector.accumulated_header_size
2344                    > self.flood_detector.config.max_header_list_size
2345                {
2346                    error!(
2347                        "{} CONTINUATION accumulated header size {} exceeds {}",
2348                        log_context!(self),
2349                        self.flood_detector.accumulated_header_size,
2350                        self.flood_detector.config.max_header_list_size
2351                    );
2352                    if (payload_len as usize) > self.zero.storage.available_space() {
2353                        return self.goaway(H2Error::EnhanceYourCalm);
2354                    }
2355                    // Remove the already-created stream slot before refusing,
2356                    // so it does not leak against MAX_CONCURRENT_STREAMS. Route
2357                    // through `remove_dead_stream` so the expect_write/read
2358                    // invariant (§LIFECYCLE.md 5.4) holds on this path too.
2359                    if let Some(global_stream_id) = self.streams.get(&stream_id).copied() {
2360                        self.remove_dead_stream(stream_id, global_stream_id);
2361                    }
2362                    return self.refuse_stream_and_discard(
2363                        stream_id,
2364                        H2Error::RefusedStream,
2365                        payload_len,
2366                    );
2367                }
2368                if (payload_len as usize) > self.zero.storage.available_space() {
2369                    error!(
2370                        "{} CONTINUATION payload {} exceeds buffer space {}",
2371                        log_context!(self),
2372                        payload_len,
2373                        self.zero.storage.available_space()
2374                    );
2375                    return self.goaway(H2Error::EnhanceYourCalm);
2376                }
2377                self.expect_read = Some((H2StreamId::Zero, payload_len as usize));
2378                let mut headers = headers.clone();
2379                headers.end_headers = flags & parser::FLAG_END_HEADERS != 0;
2380                headers.header_block_fragment.len = headers
2381                    .header_block_fragment
2382                    .len
2383                    .saturating_add(payload_len);
2384                self.state = H2State::ContinuationFrame(headers);
2385            }
2386            Err(error) => {
2387                let error = error_nom_to_h2(error);
2388                error!("{} COULD NOT PARSE CONTINUATION HEADER", log_context!(self));
2389                return self.goaway(error);
2390            }
2391            other => {
2392                error!(
2393                    "{} UNEXPECTED {:?} WHILE PARSING CONTINUATION HEADER",
2394                    log_context!(self),
2395                    other
2396                );
2397                return self.goaway(H2Error::ProtocolError);
2398            }
2399        };
2400        MuxResult::Continue
2401    }
2402
2403    pub fn readable<E, L>(&mut self, context: &mut Context<L>, mut endpoint: E) -> MuxResult
2404    where
2405        E: Endpoint,
2406        L: ListenerHandler + L7ListenerHandler,
2407    {
2408        self.prune_inactive_streams_while_closing(context);
2409        // Pass 4 Medium #3: per-stream idle guard. Slow-multiplex Slowloris
2410        // sends one byte or a control frame per stream just often enough to
2411        // reset the connection-level timer; per-stream deadlines catch it.
2412        self.cancel_timed_out_streams(context, &mut endpoint);
2413
2414        // RFC 9113 §6.5: check if peer has timed out on SETTINGS ACK
2415        if let Some(sent_at) = self.settings_sent_at
2416            && sent_at.elapsed() >= SETTINGS_ACK_TIMEOUT
2417        {
2418            warn!(
2419                "{} SETTINGS ACK timeout: no SETTINGS ACK observed within {:?}",
2420                log_context!(self),
2421                SETTINGS_ACK_TIMEOUT
2422            );
2423            return self.goaway(H2Error::SettingsTimeout);
2424        }
2425
2426        // Don't reset the timeout unconditionally here. Only application data
2427        // (DATA/HEADERS frames) should reset the timeout. H2 control frames
2428        // (PING, WINDOW_UPDATE, SETTINGS) must NOT reset it, otherwise a peer
2429        // sending periodic PINGs prevents timeout detection on stuck sessions.
2430        // The timeout is reset:
2431        // - Below, when reading DATA payload (H2StreamId::Other)
2432        // - In handle_frame(), when processing HEADERS frames
2433        let (stream_id, kawa) = if let Some((stream_id, amount)) = self.expect_read {
2434            let (kawa, did) = match stream_id {
2435                H2StreamId::Zero => (&mut self.zero, usize::MAX),
2436                H2StreamId::Other {
2437                    gid: global_stream_id,
2438                    ..
2439                } => {
2440                    // Reading DATA frame payload for an application stream.
2441                    // This is real application activity — reset the timeout.
2442                    self.timeout_container.reset();
2443                    (
2444                        context.streams[global_stream_id]
2445                            .split(&self.position)
2446                            .rbuffer,
2447                        global_stream_id,
2448                    )
2449                }
2450            };
2451            trace!(
2452                "{} {:?}({:?}, {})",
2453                log_context!(self),
2454                self.state,
2455                stream_id,
2456                amount
2457            );
2458            if amount > 0 {
2459                if amount > kawa.storage.available_space() {
2460                    self.readiness.interest.remove(Ready::READABLE);
2461                    return MuxResult::Continue;
2462                }
2463                let (size, status) = self.socket.socket_read(&mut kawa.storage.space()[..amount]);
2464                context.debug.push(DebugEvent::SocketIO(0, did, size));
2465                kawa.storage.fill(size);
2466                self.position.count_bytes_in_counter(size);
2467                self.bytes.zero_bytes_read += size;
2468                if update_readiness_after_read(size, status, &mut self.readiness) {
2469                    if matches!(self.position, Position::Server)
2470                        && self.drain.draining
2471                        && matches!(status, SocketResult::Closed | SocketResult::Error)
2472                    {
2473                        // During graceful drain, a frontend EOF/HUP means no
2474                        // further frame headers or payload bytes can arrive.
2475                        // Keeping expect_read here strands the connection in
2476                        // Header/Frame forever even after the peer is gone.
2477                        self.expect_read = None;
2478                    }
2479                    return MuxResult::Continue;
2480                } else if size == amount {
2481                    self.expect_read = None;
2482                } else {
2483                    self.expect_read = Some((stream_id, amount - size));
2484                    if let (H2State::ClientPreface, Position::Server) =
2485                        (&self.state, &self.position)
2486                    {
2487                        let i = kawa.storage.data();
2488                        if !b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n".starts_with(i) {
2489                            debug!("{} EARLY INVALID PREFACE: {:?}", log_context!(self), i);
2490                            return self.force_disconnect();
2491                        }
2492                    }
2493                    return MuxResult::Continue;
2494                }
2495            } else {
2496                self.expect_read = None;
2497            }
2498            (stream_id, kawa)
2499        } else {
2500            self.readiness.event.remove(Ready::READABLE);
2501            return MuxResult::Continue;
2502        };
2503        match (&self.state, &self.position) {
2504            (H2State::Error, _)
2505            | (H2State::GoAway, _)
2506            | (H2State::ServerSettings, Position::Server)
2507            | (H2State::ClientPreface, Position::Client(..))
2508            | (H2State::ClientSettings, Position::Client(..)) => {
2509                error!(
2510                    "{} Unexpected combination: (Readable, {:?}, {:?})",
2511                    log_context!(self),
2512                    self.state,
2513                    self.position
2514                );
2515                return self.force_disconnect();
2516            }
2517            (H2State::Discard, _) => {
2518                let _i = kawa.storage.data();
2519                trace!("{} DISCARDING: {:?}", log_context!(self), _i);
2520                kawa.storage.clear();
2521                self.attribute_bytes_to_overhead();
2522                self.expect_header();
2523            }
2524            (H2State::ClientPreface, Position::Server) => {
2525                let i = kawa.storage.data();
2526                let i = match parser::preface(i) {
2527                    Ok((i, _)) => i,
2528                    Err(_) => return self.force_disconnect(),
2529                };
2530                match parser::frame_header(i, self.local_settings.settings_max_frame_size) {
2531                    Ok((
2532                        _,
2533                        FrameHeader {
2534                            payload_len,
2535                            frame_type: FrameType::Settings,
2536                            flags: 0,
2537                            stream_id: 0,
2538                        },
2539                    )) => {
2540                        kawa.storage.clear();
2541                        self.state = H2State::ClientSettings;
2542                        self.expect_read = Some((H2StreamId::Zero, payload_len as usize));
2543                    }
2544                    _ => return self.force_disconnect(),
2545                };
2546            }
2547            (H2State::ClientSettings, Position::Server) => {
2548                let i = kawa.storage.data();
2549                let settings = match parser::settings_frame(
2550                    i,
2551                    &FrameHeader {
2552                        payload_len: i.len() as u32,
2553                        frame_type: FrameType::Settings,
2554                        flags: 0,
2555                        stream_id: 0,
2556                    },
2557                ) {
2558                    Ok((_, settings)) => {
2559                        kawa.storage.clear();
2560                        settings
2561                    }
2562                    Err(_) => return self.force_disconnect(),
2563                };
2564                let kawa = &mut self.zero;
2565                match serializer::gen_settings(kawa.storage.space(), &self.local_settings) {
2566                    Ok((_, size)) => {
2567                        kawa.storage.fill(size);
2568                        incr!(names::h2::FRAMES_TX_SETTINGS);
2569                        // RFC 9113 §6.5: start tracking SETTINGS ACK timeout
2570                        self.settings_sent_at = Some(Instant::now());
2571                    }
2572                    Err(error) => {
2573                        error!(
2574                            "{} Could not serialize SettingsFrame: {:?}",
2575                            log_context!(self),
2576                            error
2577                        );
2578                        return self.force_disconnect();
2579                    }
2580                };
2581
2582                self.state = H2State::ServerSettings;
2583                self.expect_write = Some(H2StreamId::Zero);
2584                self.readiness.signal_pending_write();
2585                return self.handle_frame(settings, 0, context, endpoint);
2586            }
2587            (H2State::ServerSettings, Position::Client(..)) => {
2588                let i = kawa.storage.data();
2589                match parser::frame_header(i, self.local_settings.settings_max_frame_size) {
2590                    Ok((
2591                        _,
2592                        header @ FrameHeader {
2593                            payload_len,
2594                            frame_type: FrameType::Settings,
2595                            flags: 0,
2596                            stream_id: 0,
2597                        },
2598                    )) => {
2599                        kawa.storage.clear();
2600                        self.expect_read = Some((H2StreamId::Zero, payload_len as usize));
2601                        self.state = H2State::Frame(header)
2602                    }
2603                    _ => return self.force_disconnect(),
2604                };
2605            }
2606            (H2State::Header, _) => {
2607                return self.handle_header_state(context);
2608            }
2609            (H2State::ContinuationHeader(headers), _) => {
2610                let headers = headers.clone();
2611                return self.handle_continuation_header_state(&headers);
2612            }
2613            (H2State::Frame(header), _) => {
2614                let i = kawa.storage.unparsed_data();
2615                trace!("{}   data: {:?}", log_context!(self), i);
2616                let wire_payload_len = header.payload_len;
2617                let frame = match parser::frame_body(i, header) {
2618                    Ok((_, frame)) => frame,
2619                    Err(error) => {
2620                        let error = error_nom_to_h2(error);
2621                        error!("{} COULD NOT PARSE FRAME BODY", log_context!(self));
2622                        return self.goaway(error);
2623                    }
2624                };
2625                if let H2StreamId::Zero = stream_id {
2626                    if header.frame_type == FrameType::Headers {
2627                        kawa.storage.head = kawa.storage.end;
2628                    } else {
2629                        kawa.storage.end = kawa.storage.head;
2630                    }
2631                }
2632                self.expect_header();
2633                return self.handle_frame(frame, wire_payload_len, context, endpoint);
2634            }
2635            (H2State::ContinuationFrame(headers), _) => {
2636                kawa.storage.head = kawa.storage.end;
2637                let i = kawa.storage.data();
2638                trace!("{}   data: {:?}", log_context!(self), i);
2639                let headers = headers.clone();
2640                self.expect_header();
2641                return self.handle_frame(Frame::Headers(headers), 0, context, endpoint);
2642            }
2643        }
2644        MuxResult::Continue
2645    }
2646
2647    /// Update the H2 connection-level *aggregate* gauges with this connection's
2648    /// current contribution, expressed as a signed delta against the last
2649    /// snapshot we emitted.
2650    ///
2651    /// The three metrics are emitted via [`gauge_add!`] (lifecycle deltas) so
2652    /// that the dashboard sees the **sum across all live H2 connections**:
2653    ///
2654    /// - `h2.connection.window_bytes` — sum of available connection-level
2655    ///   send-window bytes. Negative per-connection windows clamp to 0 so the
2656    ///   aggregate represents only available capacity, not deficit.
2657    /// - `h2.connection.active_streams` — sum of in-flight streams across
2658    ///   every H2 connection.
2659    /// - `h2.connection.pending_window_updates` — sum of queued (un-flushed)
2660    ///   per-stream WINDOW_UPDATE entries across every H2 connection.
2661    ///
2662    /// Called from the write hot path; emits nothing when the snapshot is
2663    /// unchanged so the steady state stays cheap. The paired decrement for
2664    /// every increment is provided by [`Drop`], which subtracts the final
2665    /// snapshot when the connection is dropped — keeping the aggregate
2666    /// arithmetically symmetric independent of which close path runs
2667    /// (`graceful_goaway`, `force_disconnect`, `handle_goaway_frame`,
2668    /// `Mux::close`, panic-unwind, …).
2669    fn gauge_connection_state(&mut self) {
2670        let snapshot = (
2671            self.flow_control.window.max(0) as usize,
2672            self.streams.len(),
2673            self.flow_control.pending_window_updates.len(),
2674        );
2675        if self.last_gauge_snapshot == Some(snapshot) {
2676            return;
2677        }
2678        let prev = self.last_gauge_snapshot.unwrap_or((0, 0, 0));
2679        // Diff in i64 — usize cannot represent the negative side of the delta.
2680        let dw = snapshot.0 as i64 - prev.0 as i64;
2681        let ds = snapshot.1 as i64 - prev.1 as i64;
2682        let du = snapshot.2 as i64 - prev.2 as i64;
2683        if dw != 0 {
2684            gauge_add!(names::h2::CONNECTION_WINDOW_BYTES, dw);
2685        }
2686        if ds != 0 {
2687            gauge_add!(names::h2::CONNECTION_ACTIVE_STREAMS, ds);
2688        }
2689        if du != 0 {
2690            gauge_add!(names::h2::CONNECTION_PENDING_WINDOW_UPDATES, du);
2691        }
2692        self.last_gauge_snapshot = Some(snapshot);
2693    }
2694
2695    /// Subtract this connection's contribution from the three aggregate
2696    /// `h2.connection.*` gauges. Idempotent: clears `last_gauge_snapshot` so a
2697    /// second call (or a [`Drop`] on top of an explicit reset) is a no-op.
2698    ///
2699    /// Pairs with every prior call to [`Self::gauge_connection_state`]; called
2700    /// from [`Drop`] so the symmetry is guaranteed regardless of the close
2701    /// path.
2702    fn release_connection_gauges(&mut self) {
2703        if let Some((w, s, u)) = self.last_gauge_snapshot.take() {
2704            if w != 0 {
2705                gauge_add!(names::h2::CONNECTION_WINDOW_BYTES, -(w as i64));
2706            }
2707            if s != 0 {
2708                gauge_add!(names::h2::CONNECTION_ACTIVE_STREAMS, -(s as i64));
2709            }
2710            if u != 0 {
2711                gauge_add!(names::h2::CONNECTION_PENDING_WINDOW_UPDATES, -(u as i64));
2712            }
2713        }
2714    }
2715
2716    /// Write application data (request/response bodies, headers) across all
2717    /// active streams, respecting priority ordering and flow control.
2718    ///
2719    /// This is the main data-plane write path: it resumes any partially-written
2720    /// stream, prepares new frames via the H2 block converter, flushes them to
2721    /// the socket, and recycles completed streams.
2722    ///
2723    /// NOTE: The priority iteration loop and converter setup remain inline here
2724    /// because the converter borrows `self.encoder`, preventing further
2725    /// decomposition into `&mut self` methods within the loop body.
2726    fn write_streams<E, L>(&mut self, context: &mut Context<L>, mut endpoint: E) -> MuxResult
2727    where
2728        E: Endpoint,
2729        L: ListenerHandler + L7ListenerHandler,
2730    {
2731        self.timeout_container.reset();
2732        // Pre-compute byte totals for proportional overhead distribution.
2733        let byte_totals = self.compute_stream_byte_totals(context);
2734        let mut io_slices: Vec<IoSlice<'static>> = Vec::new();
2735
2736        if let Some(
2737            write_stream @ H2StreamId::Other {
2738                id: stream_id,
2739                gid: global_stream_id,
2740            },
2741        ) = self.expect_write
2742        {
2743            let stream = &mut context.streams[global_stream_id];
2744            let stream_state = stream.state;
2745            let parts = stream.split(&self.position);
2746            let kawa = parts.wbuffer;
2747            // Resume path: if the same stream is parked waiting for buffer
2748            // space (expect_read matches write_stream), pass the amount so
2749            // flush_stream_out can re-enable READABLE as soon as we drain.
2750            let cross_read_amount = match self.expect_read {
2751                Some((read_stream, amount)) if write_stream == read_stream => Some(amount),
2752                _ => None,
2753            };
2754            let mut resume_bytes: usize = 0;
2755            let outcome = Self::flush_stream_out(
2756                &mut self.socket,
2757                kawa,
2758                parts.metrics,
2759                &self.position,
2760                &mut self.readiness,
2761                &mut context.debug,
2762                2,
2763                global_stream_id,
2764                None,
2765                cross_read_amount,
2766                &mut io_slices,
2767                Some(&mut resume_bytes),
2768            );
2769            // Refresh the per-stream idle timer when outbound bytes move: a
2770            // large response delivered at low bandwidth is "active", not idle,
2771            // even when the peer sends no inbound frames.
2772            if resume_bytes > 0 {
2773                if let Some(t) = self.stream_last_activity_at.get_mut(&stream_id) {
2774                    *t = Instant::now();
2775                }
2776                // Clear the flow-control-stall deadline ONLY when the effective
2777                // send window is genuinely open — that alone is a real un-stall.
2778                // A window-stalled stream can flush a `WINDOW_UPDATE(+1)`-drip
2779                // byte HERE via socket-backpressure resume; clearing on that
2780                // would reset the deadline at 1-byte granularity and re-open the
2781                // drip the M2 cumulative-stall budget closes. While still blocked,
2782                // leave the deadline (and its progress accumulator) for the main
2783                // write loop's budget to govern — keeping the two maps in lockstep.
2784                if min(*parts.window, self.flow_control.window) > 0 {
2785                    self.stream_fc_stalled_since.remove(&stream_id);
2786                    self.stream_fc_stalled_progress.remove(&stream_id);
2787                }
2788            }
2789            if outcome == FlushOutcome::Stalled {
2790                return MuxResult::Continue;
2791            }
2792            self.expect_write = None;
2793            if (kawa.is_terminated() || kawa.is_error())
2794                && kawa.is_completed()
2795                && !Self::handle_1xx_reset(kawa, stream_state, &mut endpoint)
2796            {
2797                let (client_rtt, server_rtt) = Self::snapshot_rtts(
2798                    &self.position,
2799                    &self.socket,
2800                    &endpoint,
2801                    stream.linked_token(),
2802                );
2803
2804                if let Some((dead_id, token)) = Self::try_recycle_server_stream(
2805                    &self.position,
2806                    &mut self.bytes,
2807                    &self.streams,
2808                    stream,
2809                    global_stream_id,
2810                    stream_id,
2811                    byte_totals,
2812                    &mut context.debug,
2813                    context.listener.clone(),
2814                    client_rtt,
2815                    server_rtt,
2816                ) {
2817                    // Remove the recycled stream from the connection maps
2818                    // before endpoint.end_stream() can trigger teardown.
2819                    // Otherwise session close can observe a stale `Recycle`
2820                    // entry in self.streams and mis-handle the connection as
2821                    // if it still had an active H2 stream.
2822                    self.remove_dead_stream(dead_id, global_stream_id);
2823                    if let Some(token) = token {
2824                        remove_backend_stream(
2825                            &mut context.backend_streams,
2826                            token,
2827                            global_stream_id,
2828                        );
2829                        endpoint.end_stream(token, global_stream_id, context);
2830                    }
2831                }
2832            }
2833        }
2834
2835        self.gauge_connection_state();
2836
2837        let scheme: &'static [u8] = if context.listener.borrow().protocol() == Protocol::HTTPS {
2838            b"https"
2839        } else {
2840            b"http"
2841        };
2842        let mut completed_streams = Vec::new();
2843        let mut converter_buf = std::mem::take(&mut self.converter_buf);
2844        converter_buf.clear();
2845        let mut converter = converter::H2BlockConverter {
2846            max_frame_size: self.peer_settings.settings_max_frame_size as usize,
2847            window: 0,
2848            stream_id: 0,
2849            encoder: &mut self.encoder,
2850            out: converter_buf,
2851            scheme,
2852            lowercase_buf: std::mem::take(&mut self.lowercase_buf),
2853            cookie_buf: std::mem::take(&mut self.cookie_buf),
2854            // When this connection is a backend client we are writing
2855            // toward the upstream backend — flow-control stalls in that
2856            // direction are scoped to `backend.flow_control.paused` (in
2857            // addition to the existing direction-agnostic
2858            // `h2.flow_control_stall`).
2859            position_is_client: self.position.is_client(),
2860            // RFC 9218 §4: toggled per-stream in the loop below, driven by
2861            // `Prioriser::get(stream_id).1`. Non-incremental by default so
2862            // unit tests and non-scheduled callers (e.g. the resume path
2863            // above) keep the sequential semantics.
2864            incremental_mode: false,
2865            // Populated once per write pass from `apply_incremental_rotation`
2866            // below. The converter uses `incremental_peer_count <= 1` to skip
2867            // the RFC 9218 yield-after-one-DATA behaviour when there is no
2868            // peer to interleave with (solo-bucket fast path).
2869            incremental_peer_count: 0,
2870            // RFC 7541 §6.3: move the pending size-update onto the converter
2871            // so the first header block of this pass prepends the signal.
2872            // We clear the connection-side mirror only AFTER the write pass
2873            // confirms emission via `converter.size_update_emitted`, so a
2874            // DATA-only write pass (no header block) does not drop the
2875            // signal.
2876            pending_table_size_update: self.pending_table_size_update,
2877            size_update_emitted: false,
2878            // Reset on every write pass; `check_header_capacity` flips it
2879            // mid-call and `finalize` commits the abort by flipping
2880            // `kawa.parsing_phase` to Error so the next pass emits
2881            // RST_STREAM(InternalError).
2882            pending_oversized_abort: false,
2883        };
2884        self.priorities_buf.clear();
2885        self.priorities_buf.extend(self.streams.keys().copied());
2886        // RFC 9218 §4 primary sort: ascending urgency, then stream ID for
2887        // stability. The incremental flag is handled by
2888        // `apply_incremental_rotation` below so it does not perturb the
2889        // non-incremental fast path.
2890        self.priorities_buf.sort_by_cached_key(|id| {
2891            let (urgency, _) = self.prioriser.get(id);
2892            (urgency, *id)
2893        });
2894        // RFC 9218 §4: inside each urgency bucket, move incremental streams
2895        // to the tail and rotate them by the per-connection round-robin
2896        // cursor so no single slow-draining stream can starve its
2897        // same-urgency incremental peers.
2898        let incremental_count = self
2899            .prioriser
2900            .apply_incremental_rotation(&mut self.priorities_buf);
2901
2902        // RFC 9218 §4 refinement (Tier 3a): the connection-global
2903        // `incremental_count` is too coarse for `converter.incremental_peer_count`.
2904        // A solo `u=0, i` stream with an unrelated `u=7, i` peer in a
2905        // different urgency bucket would still see `incremental_peer_count > 1`
2906        // and voluntarily yield — stranding bytes the invariant-15/16 guards
2907        // were meant to prevent. Scope the count to same-urgency streams that
2908        // are actually ready to emit this pass (eligibility mirrors the check
2909        // in the write loop below).
2910        let mut ready_incremental_by_urgency: HashMap<u8, usize> = HashMap::new();
2911        for &sid in self.priorities_buf.iter() {
2912            let (urgency, is_incremental) = self.prioriser.get(&sid);
2913            if !is_incremental {
2914                continue;
2915            }
2916            let Some(&gid) = self.streams.get(&sid) else {
2917                continue;
2918            };
2919            let wbuffer = match self.position {
2920                Position::Server => &context.streams[gid].back,
2921                Position::Client(..) => &context.streams[gid].front,
2922            };
2923            if wbuffer.is_main_phase()
2924                || (wbuffer.is_terminated() && !wbuffer.is_completed())
2925                || (wbuffer.is_error() && !self.rst_sent.contains(&sid))
2926            {
2927                *ready_incremental_by_urgency.entry(urgency).or_insert(0) += 1;
2928            }
2929        }
2930
2931        trace!(
2932            "{} PRIORITIES: {:?} (incremental_count={}, per_bucket={:?})",
2933            log_context!(self),
2934            self.priorities_buf,
2935            incremental_count,
2936            ready_incremental_by_urgency
2937        );
2938        let mut socket_write = false;
2939        // RFC 9218 §4 round-robin: remember the first incremental stream we
2940        // served this pass so we can advance `Prioriser::incremental_cursor`
2941        // to it, causing the next pass to start with the stream just after.
2942        let mut first_incremental_fired: Option<StreamId> = None;
2943        // Total outbound bytes emitted across all stream flushes this pass —
2944        // `finalize_write` uses this to distinguish a voluntary scheduler
2945        // yield (progress + pending back-buffer, LIFECYCLE §9 invariant 16)
2946        // from a no-progress wait state (e.g. flow-control starvation).
2947        let mut total_bytes_written: usize = 0;
2948        // Collect every fresh RST_STREAM emitted via the converter
2949        // (`initialize` chokepoint or the HPACK over-budget abort path)
2950        // so we can run `account_emitted_rst` for each one AFTER the
2951        // converter is dropped — the converter holds `&mut self.encoder`
2952        // for the loop body so we cannot take `&mut self` until then.
2953        let mut freshly_emitted_rsts: Vec<H2Error> = Vec::new();
2954        'outer: for idx in 0..self.priorities_buf.len() {
2955            let stream_id = self.priorities_buf[idx];
2956            let Some(&global_stream_id) = self.streams.get(&stream_id) else {
2957                error!(
2958                    "{} stream_id {} from sorted keys missing in streams map",
2959                    log_context!(self),
2960                    stream_id
2961                );
2962                continue;
2963            };
2964            let (urgency, is_incremental) = self.prioriser.get(&stream_id);
2965            let stream = &mut context.streams[global_stream_id];
2966            let stream_state = stream.state;
2967            let parts = stream.split(&self.position);
2968            let kawa = parts.wbuffer;
2969            // Hoisted out of the gate below so the post-flush flow-control-stall
2970            // classification can see how many flow-control bytes this pass moved.
2971            let mut consumed: i32 = 0;
2972            if kawa.is_main_phase()
2973                || (kawa.is_terminated() && !kawa.is_completed())
2974                || (kawa.is_error() && !self.rst_sent.contains(&stream_id))
2975            {
2976                let window = min(*parts.window, self.flow_control.window);
2977                converter.window = window;
2978                converter.stream_id = stream_id;
2979                // RFC 9218 §4: incremental streams yield the converter after
2980                // a single DATA frame so same-urgency peers interleave.
2981                converter.incremental_mode = is_incremental;
2982                // Same-urgency-bucket ready-peer count (Tier 3a, LIFECYCLE §9
2983                // invariant 17). The converter skips the yield when there is
2984                // no peer in the same bucket to interleave with — prevents
2985                // the `finalize_write` WRITABLE-withdrawal strand (see
2986                // `test_h2_solo_incremental_drains_fully`). A connection-wide
2987                // count would wrongly yield for a solo incremental stream
2988                // when another urgency bucket happens to contain an
2989                // incremental peer.
2990                converter.incremental_peer_count = ready_incremental_by_urgency
2991                    .get(&urgency)
2992                    .copied()
2993                    .unwrap_or(0);
2994                // Track RST_STREAM dedup: if kawa is in error state, the converter
2995                // will generate a RST_STREAM frame via `initialize`. Mark it so we
2996                // don't send a duplicate on the next writable cycle.
2997                if kawa.is_error() {
2998                    let freshly_rst = self.rst_sent.insert(stream_id);
2999                    // LIFECYCLE §9 invariant 17: any transition to ineligible
3000                    // mid-pass MUST decrement ready_incremental_by_urgency so
3001                    // later streams in the same 'outer iteration see the live
3002                    // count, not the snapshot. Missing this costs one voluntary
3003                    // yield per same-urgency peer that trails the RST.
3004                    if freshly_rst
3005                        && is_incremental
3006                        && let Some(c) = ready_incremental_by_urgency.get_mut(&urgency)
3007                    {
3008                        *c = c.saturating_sub(1);
3009                    }
3010                    // Account for the RST that `initialize` is about to emit
3011                    // for this stream. Without this the MadeYouReset lifetime
3012                    // cap is evadable: any path that flips `parsing_phase` to
3013                    // Error before reaching this gate (oversized inbound
3014                    // trailers, malformed bodies, etc.) would land an
3015                    // unaccounted RST on the wire. We defer the actual
3016                    // accounting call until after `drop(converter)` — the
3017                    // converter holds `&mut self.encoder` here.
3018                    if freshly_rst {
3019                        freshly_emitted_rsts.push(rst_error_from_kawa(kawa));
3020                    }
3021                }
3022                // Apply per-frontend response-side header edits
3023                // (set/replace/delete) stashed by the routing layer at
3024                // request time. H2 frontends always run as Server
3025                // position; the back-side H2 client (when sozu speaks
3026                // H2 to a backend) is a request emission and was
3027                // already mutated by Router::route_from_request.
3028                //
3029                // The snapshot is **drained** via `mem::take` so the
3030                // injection runs exactly once per response. Without
3031                // this, a re-entry of `write_streams` for the same
3032                // stream (multi-frame body, flow-control yield, or
3033                // RFC 9218 same-urgency round-robin) would re-call
3034                // `apply_response_header_edits` after `kawa.prepare`
3035                // had already consumed the `Block::Flags{end_header}`
3036                // anchor — the helper falls back to
3037                // `kawa.blocks.len()` and appends the edit AFTER all
3038                // remaining DATA blocks. The next prepare cycle then
3039                // encodes that orphan `Block::Header` into
3040                // `H2BlockConverter.out` with no closing
3041                // `Block::Flags{end_header}` to flush it as a HEADERS
3042                // frame, and `H2BlockConverter::finalize` trips the
3043                // "out buffer not empty (38 bytes remaining), clearing"
3044                // defense-in-depth log on every re-entry. 38 bytes is
3045                // the static-table HPACK encoding of a typical HSTS
3046                // header, which is how the symptom surfaces in
3047                // production once the listener-default HSTS reaches a
3048                // non-trivial share of frontends.
3049                if matches!(self.position, super::Position::Server)
3050                    && !parts.context.headers_response.is_empty()
3051                {
3052                    let edits = std::mem::take(&mut parts.context.headers_response);
3053                    super::shared::apply_response_header_edits(kawa, &edits);
3054                }
3055                kawa.prepare(&mut converter);
3056                // The pre-prepare gate at line 2483 only inserts into
3057                // `rst_sent` when `kawa.is_error()` is already true on
3058                // entry. The HPACK over-budget abort path
3059                // (`H2BlockConverter::check_header_capacity` →
3060                // `finalize`) flips `parsing_phase` to Error AND pushes
3061                // its own RST_STREAM frame inside this same prepare
3062                // pass; without a post-prepare insert here the next
3063                // writable cycle would gate-pass and double-emit a
3064                // RST_STREAM via the existing `initialize` chokepoint.
3065                //
3066                // Per Codex P2: the converter's direct RST emission
3067                // bypasses the metric/flood accounting that
3068                // `Self::reset_stream` performs. Mirror it here so a
3069                // peer that drives oversized headers across many
3070                // streams cannot escape the MadeYouReset emitted-RST
3071                // lifetime cap and so dashboards see the per-error
3072                // counter and the global tx counter.
3073                //
3074                // Per Codex P3: when an incremental stream flips to
3075                // Error mid-prepare, the RFC 9218 §4 yield-after-one
3076                // accounting must drop this stream from the
3077                // same-urgency ready bucket so trailing peers see the
3078                // live count.
3079                let freshly_rst_post_prepare = kawa.is_error() && self.rst_sent.insert(stream_id);
3080                if freshly_rst_post_prepare {
3081                    // Defer accounting until after `drop(converter)`; same
3082                    // reason as the pre-prepare collector above.
3083                    freshly_emitted_rsts.push(rst_error_from_kawa(kawa));
3084                    if is_incremental
3085                        && let Some(c) = ready_incremental_by_urgency.get_mut(&urgency)
3086                    {
3087                        *c = c.saturating_sub(1);
3088                    }
3089                }
3090                consumed = window - converter.window;
3091                *parts.window = parts.window.saturating_sub(consumed);
3092                self.flow_control.window = self.flow_control.window.saturating_sub(consumed);
3093                if is_incremental && consumed > 0 && first_incremental_fired.is_none() {
3094                    first_incremental_fired = Some(stream_id);
3095                }
3096            }
3097            context.debug.push(DebugEvent::S(
3098                stream_id,
3099                global_stream_id,
3100                kawa.parsing_phase,
3101                kawa.blocks.len(),
3102                kawa.out.len(),
3103            ));
3104            let mut stream_bytes: usize = 0;
3105            let outcome = Self::flush_stream_out(
3106                &mut self.socket,
3107                kawa,
3108                parts.metrics,
3109                &self.position,
3110                &mut self.readiness,
3111                &mut context.debug,
3112                3,
3113                global_stream_id,
3114                Some(&mut socket_write),
3115                None,
3116                &mut io_slices,
3117                Some(&mut stream_bytes),
3118            );
3119            // Refresh the per-stream idle timer on outbound bytes. Without
3120            // this, a long-running response trickled at low bandwidth would
3121            // be killed by `cancel_timed_out_streams` mid-delivery — the
3122            // inbound-only refresh at h2.rs:3887-3895 / 4026-4031 never
3123            // fires while the peer is idle.
3124            if stream_bytes > 0
3125                && let Some(t) = self.stream_last_activity_at.get_mut(&stream_id)
3126            {
3127                *t = Instant::now();
3128            }
3129            // Arm/age the dedicated flow-control-stall deadline that catches a
3130            // window-stalled stream — a buffered RESPONSE to a slow frontend
3131            // (`Position::Server`) OR a buffered request UPLOAD to a slow H2
3132            // backend (`Position::Client`): window-stall reaping is bidirectional
3133            // by design (M4), so there is no position gate here. Set only when the
3134            // stream holds sendable buffered data it cannot send because its
3135            // effective send window is exhausted; unlike `stream_last_activity_at`
3136            // it is NEVER refreshed by inbound DATA/HEADERS, so a peer dribbling
3137            // 1-byte DATA cannot keep it warm.
3138            //
3139            // M2 cumulative-stall budget: a genuinely OPEN window clears the
3140            // deadline immediately (real un-stall). While the window stays
3141            // blocked, accumulate this pass's outbound drain; only cumulative
3142            // progress reaching `FC_STALL_CLEAR_FLOOR` (a full frame of real
3143            // delivery) clears it. A `WINDOW_UPDATE(+1)` drip drains ~1 byte/pass
3144            // straight back to a zero window, so it never reaches the floor — the
3145            // deadline ages out and `cancel_timed_out_streams` RST(CANCEL)s the
3146            // slot-pinning stream after `stream_idle_timeout`.
3147            let outbound_window_blocked = has_sendable_response(kawa)
3148                && min(*parts.window, self.flow_control.window) <= 0
3149                && (!kawa.blocks.is_empty() || !kawa.out.is_empty());
3150            match fc_stall_budget_decision(
3151                outbound_window_blocked,
3152                consumed,
3153                self.stream_fc_stalled_progress.get(&stream_id).copied(),
3154            ) {
3155                FcStallAction::Clear => {
3156                    self.stream_fc_stalled_since.remove(&stream_id);
3157                    self.stream_fc_stalled_progress.remove(&stream_id);
3158                }
3159                FcStallAction::Arm { progress } => {
3160                    self.stream_fc_stalled_since
3161                        .entry(stream_id)
3162                        .or_insert_with(Instant::now);
3163                    self.stream_fc_stalled_progress.insert(stream_id, progress);
3164                }
3165            }
3166            total_bytes_written = total_bytes_written.saturating_add(stream_bytes);
3167            if outcome == FlushOutcome::Stalled {
3168                self.expect_write = Some(H2StreamId::Other {
3169                    id: stream_id,
3170                    gid: global_stream_id,
3171                });
3172                break 'outer;
3173            }
3174            self.expect_write = None;
3175            if (kawa.is_terminated() || kawa.is_error())
3176                && kawa.is_completed()
3177                && !Self::handle_1xx_reset(kawa, stream_state, &mut endpoint)
3178            {
3179                let close_frontend =
3180                    matches!(self.position, Position::Server) && !parts.context.keep_alive_frontend;
3181                let (client_rtt, server_rtt) = Self::snapshot_rtts(
3182                    &self.position,
3183                    &self.socket,
3184                    &endpoint,
3185                    stream.linked_token(),
3186                );
3187
3188                if let Some((dead_id, token)) = Self::try_recycle_server_stream(
3189                    &self.position,
3190                    &mut self.bytes,
3191                    &self.streams,
3192                    stream,
3193                    global_stream_id,
3194                    stream_id,
3195                    byte_totals,
3196                    &mut context.debug,
3197                    context.listener.clone(),
3198                    client_rtt,
3199                    server_rtt,
3200                ) {
3201                    completed_streams.push((dead_id, global_stream_id, token, close_frontend));
3202                    // LIFECYCLE §9 invariant 17: decrement INSIDE 'outer so
3203                    // later iterations see the reduced count. The post-loop
3204                    // retirement at remove_dead_stream is too late.
3205                    if is_incremental
3206                        && let Some(c) = ready_incremental_by_urgency.get_mut(&urgency)
3207                    {
3208                        *c = c.saturating_sub(1);
3209                    }
3210                }
3211            }
3212        }
3213        gauge!(
3214            "h2.streams.ready_incremental.by_urgency",
3215            ready_incremental_by_urgency
3216                .values()
3217                .copied()
3218                .sum::<usize>()
3219        );
3220        // Reclaim the converter's reusable buffers before any &mut self calls,
3221        // since the converter borrows self.encoder.
3222        let converter_out = std::mem::take(&mut converter.out);
3223        let lowercase_buf = std::mem::take(&mut converter.lowercase_buf);
3224        let cookie_buf = std::mem::take(&mut converter.cookie_buf);
3225        // RFC 7541 §6.3: clear our mirror of the pending size-update only
3226        // AFTER the converter confirmed the signal was emitted to its
3227        // output buffer. A DATA-only pass leaves `size_update_emitted` as
3228        // `false` so the signal stays queued for the next pass with a
3229        // header block.
3230        let size_update_emitted = converter.size_update_emitted;
3231        drop(converter);
3232        if size_update_emitted {
3233            self.pending_table_size_update = None;
3234        }
3235        // Account every RST that the converter emitted during this pass
3236        // (pre-prepare gate + post-prepare HPACK over-budget abort) so
3237        // the global tx counter, the per-error breakdown, and the
3238        // MadeYouReset emitted-RST lifetime cap stay in step. If the
3239        // cap trips, propagate the GOAWAY result.
3240        for error in freshly_emitted_rsts {
3241            if let Some(result) = self.account_emitted_rst(error) {
3242                return result;
3243            }
3244        }
3245        self.converter_buf = converter_out;
3246        self.lowercase_buf = lowercase_buf;
3247        self.cookie_buf = cookie_buf;
3248        self.shrink_converter_buffers();
3249        // RFC 9218 §4: commit the round-robin cursor so the next writable
3250        // cycle begins with the stream immediately after the one we fired
3251        // first this pass.
3252        self.prioriser
3253            .advance_incremental_cursor(first_incremental_fired);
3254        let mut close_frontend_after_completed_stream = false;
3255        for (dead_id, global_stream_id, token, close_frontend) in completed_streams {
3256            // The main write loop borrows self.encoder, so we can't mutate the
3257            // H2 maps inline. Retire the recycled stream immediately after the
3258            // converter borrow ends, before endpoint.end_stream() can trigger
3259            // teardown and observe a stale `Recycle` entry in self.streams.
3260            self.remove_dead_stream(dead_id, global_stream_id);
3261            close_frontend_after_completed_stream |= close_frontend;
3262            if let Some(token) = token {
3263                remove_backend_stream(&mut context.backend_streams, token, global_stream_id);
3264                endpoint.end_stream(token, global_stream_id, context);
3265            }
3266        }
3267        if close_frontend_after_completed_stream && !self.drain.draining {
3268            return if self.streams.is_empty() {
3269                self.goaway(H2Error::NoError)
3270            } else {
3271                self.graceful_goaway()
3272            };
3273        }
3274        self.finalize_write(socket_write, total_bytes_written, context)
3275    }
3276
3277    /// Remove streams that completed their lifecycle from all tracking maps.
3278    /// After forwarding a 1xx informational response (100 Continue, 103 Early Hints),
3279    /// reset the back buffer and re-enable backend readable so the final response
3280    /// can arrive on the same stream. Returns true if the response was 1xx.
3281    #[allow(clippy::too_many_arguments)]
3282    fn flush_stream_out(
3283        socket: &mut Front,
3284        kawa: &mut GenericHttpStream,
3285        metrics: &mut SessionMetrics,
3286        position: &Position,
3287        readiness: &mut Readiness,
3288        debug: &mut DebugHistory,
3289        debug_site: usize,
3290        global_stream_id: GlobalStreamId,
3291        mut wrote: Option<&mut bool>,
3292        cross_read_amount: Option<usize>,
3293        io_slices: &mut Vec<IoSlice<'static>>,
3294        mut bytes_written: Option<&mut usize>,
3295    ) -> FlushOutcome {
3296        while !kawa.out.is_empty() {
3297            if let Some(flag) = wrote.as_deref_mut() {
3298                *flag = true;
3299            }
3300            io_slices.clear();
3301            let buffer = kawa.storage.buffer();
3302            for block in kawa.out.iter() {
3303                match block {
3304                    kawa::OutBlock::Delimiter => break,
3305                    kawa::OutBlock::Store(store) => {
3306                        let data = store.data(buffer);
3307                        // SAFETY: the IoSlice references point into kawa's
3308                        // storage buffer. They are used only for the
3309                        // socket_write_vectored call below and cleared
3310                        // immediately after, before kawa.consume() which may
3311                        // relocate the buffer via ptr::copy (shift). No
3312                        // dangling 'static refs exist during consume().
3313                        let data: &'static [u8] =
3314                            unsafe { std::slice::from_raw_parts(data.as_ptr(), data.len()) };
3315                        io_slices.push(IoSlice::new(data));
3316                    }
3317                }
3318            }
3319            let (size, status) = socket.socket_write_vectored(io_slices);
3320            io_slices.clear();
3321            debug_assert!(
3322                io_slices.is_empty(),
3323                "IoSlice refs must be cleared before consume"
3324            );
3325            debug.push(DebugEvent::SocketIO(debug_site, global_stream_id, size));
3326            kawa.consume(size);
3327            position.count_bytes_out_counter(size);
3328            position.count_bytes_out(metrics, size);
3329            if let Some(counter) = bytes_written.as_deref_mut() {
3330                *counter = counter.saturating_add(size);
3331            }
3332            if let Some(amount) = cross_read_amount {
3333                // Resume path: same stream is parked waiting for buffer space.
3334                // Re-enable READABLE once the write freed enough room.
3335                if kawa.storage.available_space() >= amount {
3336                    readiness.interest.insert(Ready::READABLE);
3337                }
3338            }
3339            if update_readiness_after_write(size, status, readiness) {
3340                return FlushOutcome::Stalled;
3341            }
3342        }
3343        FlushOutcome::Drained
3344    }
3345
3346    fn handle_1xx_reset<E: Endpoint>(
3347        kawa: &mut GenericHttpStream,
3348        stream_state: StreamState,
3349        endpoint: &mut E,
3350    ) -> bool {
3351        let is_1xx = matches!(
3352            kawa.detached.status_line,
3353            kawa::StatusLine::Response { code, .. } if (100..200).contains(&code)
3354        );
3355        if !is_1xx {
3356            return false;
3357        }
3358        debug!(
3359            "{} H2 write_streams: 1xx informational forwarded, resetting back buffer",
3360            log_module_context!()
3361        );
3362        kawa.clear();
3363        if let StreamState::Linked(token) = stream_state {
3364            let readiness = endpoint.readiness_mut(token);
3365            readiness.interest.insert(Ready::READABLE);
3366            readiness.signal_pending_read();
3367        }
3368        true
3369    }
3370
3371    /// Re-arm edge-triggered WRITABLE event if rustls still has buffered TLS data.
3372    fn ensure_tls_flushed(&mut self) {
3373        if self.socket.socket_wants_write() {
3374            self.readiness.signal_pending_write();
3375        }
3376    }
3377
3378    /// Evict every per-stream piece of state carried by this `ConnectionH2`.
3379    ///
3380    /// **Invariant**: `rst_sent`, `stream_last_activity_at`,
3381    /// `stream_fc_stalled_since`, `stream_fc_stalled_progress` and `prioriser`
3382    /// MUST be emptied of `stream_id` here — they are the only five per-stream
3383    /// caches that are not stored in the slab-allocated
3384    /// `Context.streams[]`. Forgetting any of them causes unbounded memory
3385    /// growth on long-lived connections with many cancelled streams. The
3386    /// `debug_assert`s below fail loudly in test builds if someone adds a
3387    /// new per-stream cache without updating this function.
3388    fn remove_dead_stream(&mut self, stream_id: StreamId, global_stream_id: GlobalStreamId) {
3389        if self.streams.remove(&stream_id).is_none() {
3390            error!(
3391                "{} dead stream_id {} missing from streams map",
3392                log_context!(self),
3393                stream_id
3394            );
3395        }
3396        self.rst_sent.remove(&stream_id);
3397        self.stream_last_activity_at.remove(&stream_id);
3398        self.stream_fc_stalled_since.remove(&stream_id);
3399        self.stream_fc_stalled_progress.remove(&stream_id);
3400        self.prioriser.remove(&stream_id);
3401        debug_assert!(
3402            !self.rst_sent.contains(&stream_id),
3403            "rst_sent still contains stream_id {stream_id} after eviction"
3404        );
3405        debug_assert!(
3406            !self.stream_last_activity_at.contains_key(&stream_id),
3407            "stream_last_activity_at still contains stream_id {stream_id} after eviction"
3408        );
3409        debug_assert!(
3410            !self.stream_fc_stalled_since.contains_key(&stream_id),
3411            "stream_fc_stalled_since still contains stream_id {stream_id} after eviction"
3412        );
3413        debug_assert!(
3414            !self.stream_fc_stalled_progress.contains_key(&stream_id),
3415            "stream_fc_stalled_progress still contains stream_id {stream_id} after eviction"
3416        );
3417        // Invariant: expect_write/expect_read must not reference a gid whose
3418        // context slot may be popped by shrink_trailing_recycle after eviction.
3419        if matches!(self.expect_write, Some(H2StreamId::Other { gid, .. }) if gid == global_stream_id)
3420        {
3421            self.expect_write = None;
3422        }
3423        if matches!(
3424            self.expect_read,
3425            Some((H2StreamId::Other { gid, .. }, _)) if gid == global_stream_id
3426        ) {
3427            self.expect_read = None;
3428        }
3429    }
3430
3431    /// Drop stream-id mappings for streams that never became active before a
3432    /// connection-level close. This happens on incomplete/oversized header
3433    /// blocks: the stream slot is created on the initial HEADERS frame, then a
3434    /// GOAWAY closes the connection before the request is fully materialized.
3435    fn prune_inactive_streams_while_closing<L>(&mut self, context: &mut Context<L>)
3436    where
3437        L: ListenerHandler + L7ListenerHandler,
3438    {
3439        if !self.drain.draining || !matches!(self.state, H2State::GoAway | H2State::Error) {
3440            return;
3441        }
3442
3443        let stale_streams = self
3444            .streams
3445            .iter()
3446            .filter_map(|(&stream_id, &global_stream_id)| {
3447                (!context.streams[global_stream_id].state.is_open())
3448                    .then_some((stream_id, global_stream_id))
3449            })
3450            .collect::<Vec<_>>();
3451
3452        for (stream_id, global_stream_id) in stale_streams {
3453            let stream = &mut context.streams[global_stream_id];
3454            if stream.state == StreamState::Idle {
3455                stream.front.clear();
3456                stream.front.storage.clear();
3457                stream.back.clear();
3458                stream.back.storage.clear();
3459                stream.metrics.reset();
3460                stream.state = StreamState::Recycle;
3461            }
3462            self.remove_dead_stream(stream_id, global_stream_id);
3463        }
3464    }
3465
3466    /// Shrink reusable converter buffers when they grow beyond 16 KB to avoid
3467    /// holding memory after a burst of large headers.
3468    fn shrink_converter_buffers(&mut self) {
3469        if self.converter_buf.capacity() > 16_384 {
3470            self.converter_buf.shrink_to(4096);
3471        }
3472        if self.lowercase_buf.capacity() > 16_384 {
3473            self.lowercase_buf.shrink_to(4096);
3474        }
3475        if self.cookie_buf.capacity() > 16_384 {
3476            self.cookie_buf.shrink_to(4096);
3477        }
3478    }
3479
3480    /// Post-write phase: check drain completion, flush TLS, and update readiness.
3481    ///
3482    /// `bytes_written_this_pass` reports the total outbound bytes `write_streams`
3483    /// pushed to the socket (across every stream), and is used to distinguish
3484    /// two very different "no `expect_write`" states:
3485    ///
3486    /// - **Voluntary yield with progress**: at least one DATA/HEADERS frame
3487    ///   emitted, but a stream left non-empty `back.out`/`back.blocks` because
3488    ///   the converter yielded (e.g. RFC 9218 incremental rotation). LIFECYCLE
3489    ///   §9 invariant 16: keep `Ready::WRITABLE` armed so the session loop can
3490    ///   resume flushing on the next tick without waiting for an external
3491    ///   wake-up that edge-triggered epoll will not deliver.
3492    /// - **No progress at all**: converter pushed every block back (e.g. flow
3493    ///   window exhausted, no HEADERS ready yet). Strip `Ready::WRITABLE` —
3494    ///   forward progress must come from an external trigger
3495    ///   (`WINDOW_UPDATE`, new request), not from looping writable().
3496    ///
3497    /// Returns `MuxResult::Continue` in the normal case, or triggers a graceful
3498    /// GOAWAY when draining and all streams have completed.
3499    fn finalize_write<L>(
3500        &mut self,
3501        socket_write: bool,
3502        bytes_written_this_pass: usize,
3503        context: &mut Context<L>,
3504    ) -> MuxResult
3505    where
3506        L: ListenerHandler + L7ListenerHandler,
3507    {
3508        // RFC 9113 §6.8: if draining and all streams have completed,
3509        // send the final GOAWAY with the actual last_stream_id
3510        if self.drain.draining && self.streams.is_empty() {
3511            return self.graceful_goaway();
3512        }
3513
3514        if self.socket.socket_wants_write() {
3515            if !socket_write {
3516                self.socket.socket_write(&[]);
3517            }
3518            // Edge-triggered epoll: re-arm WRITABLE if rustls still has
3519            // pending encrypted data (first check triggers flush, second re-checks).
3520            self.ensure_tls_flushed();
3521        } else if self.expect_write.is_none() {
3522            // LIFECYCLE §9 invariant 16: retain `Ready::WRITABLE` when a
3523            // voluntary scheduler yield leaves stranded bytes in a stream's
3524            // `back.out`/`back.blocks` *after* the pass made forward
3525            // progress. Requiring progress avoids the degenerate no-progress
3526            // loop (e.g. flow-control-starved streams) that would otherwise
3527            // busy-spin against the session dispatcher.
3528            if bytes_written_this_pass > 0
3529                && any_stream_has_pending_back(&self.streams, &context.streams)
3530            {
3531                #[cfg(debug_assertions)]
3532                context.debug.push(DebugEvent::Str(
3533                    "finalize_write: invariant 16 retained WRITABLE (pending back-buffer)"
3534                        .to_owned(),
3535                ));
3536            } else if !self.pending_rst_streams.is_empty()
3537                || !self.flow_control.pending_window_updates.is_empty()
3538            {
3539                // Control-frame liveness: `flush_pending_control_frames` is
3540                // gated on `expect_write.is_none()`, so when a prior partial
3541                // write deferred the flush the RST / WINDOW_UPDATE queues
3542                // stay non-empty after `expect_write` finally drains. Without
3543                // this rearm the next tick would drop `Ready::WRITABLE` and
3544                // the queued RST would stall until an unrelated event
3545                // re-triggered writable — which is exactly the scenario
3546                // h2spec trips by sending back-to-back malformed streams.
3547                #[cfg(debug_assertions)]
3548                context.debug.push(DebugEvent::Str(
3549                    "finalize_write: retained WRITABLE (control queue non-empty)".to_owned(),
3550                ));
3551                self.readiness.arm_writable();
3552                incr!(names::h2::SIGNAL_WRITABLE_REARMED_CONTROL_QUEUE);
3553            } else {
3554                // We wrote everything
3555                #[cfg(debug_assertions)]
3556                context.debug.push(DebugEvent::Str(format!(
3557                    "Wrote everything: {:?}",
3558                    self.streams
3559                )));
3560                self.readiness.interest.remove(Ready::WRITABLE);
3561            }
3562        }
3563        MuxResult::Continue
3564    }
3565
3566    /// Flush pending control frames (zero-buffer resume, WINDOW_UPDATEs, RST_STREAMs)
3567    /// before entering the main writable state machine.
3568    ///
3569    /// Returns `Some(result)` if the caller should return early (e.g. socket would
3570    /// block, GOAWAY triggered), or `None` if writable() should proceed normally.
3571    fn flush_pending_control_frames(&mut self) -> Option<MuxResult> {
3572        if self.frontend_hung_up_while_draining() {
3573            self.expect_write = None;
3574            self.zero.storage.clear();
3575            self.flow_control.pending_window_updates.clear();
3576            self.pending_rst_streams.clear();
3577        }
3578
3579        // RFC 9113 §6.5: check if peer has timed out on SETTINGS ACK
3580        if let Some(sent_at) = self.settings_sent_at
3581            && sent_at.elapsed() >= SETTINGS_ACK_TIMEOUT
3582        {
3583            warn!(
3584                "{} SETTINGS ACK timeout: no SETTINGS ACK observed within {:?}",
3585                log_context!(self),
3586                SETTINGS_ACK_TIMEOUT
3587            );
3588            return Some(self.goaway(H2Error::SettingsTimeout));
3589        }
3590
3591        // Stage — resume zero-buffer flush.
3592        // If a previous write was partial, finish it before serialising any
3593        // new control frames. Don't reset the timeout for control frame
3594        // writes (SETTINGS ACK, PING response, WINDOW_UPDATE) — only
3595        // application-data writes should reset it.
3596        if let Some(H2StreamId::Zero) = self.expect_write {
3597            if self.flush_zero_to_socket() {
3598                self.ensure_tls_flushed();
3599                return Some(MuxResult::Continue);
3600            }
3601            // When H2StreamId::Zero is used to write, READABLE is disabled —
3602            // re-enable it now that the flush is complete.
3603            self.readiness.interest.insert(Ready::READABLE);
3604            self.expect_write = None;
3605        }
3606
3607        // Stage — drain pending WINDOW_UPDATE frames.
3608        // Serialize and flush them inline to avoid extra event loop
3609        // iterations that could cause response data to be sent before
3610        // subsequent frames are validated.
3611        if !self.flow_control.pending_window_updates.is_empty() && self.expect_write.is_none() {
3612            let kawa = &mut self.zero;
3613            kawa.storage.clear();
3614            let buf = kawa.storage.space();
3615            let mut offset = 0;
3616            // Track which entries we successfully serialized so we can remove them.
3617            // Each WINDOW_UPDATE frame is 13 bytes (9-byte header + 4-byte payload).
3618            let mut written_ids = Vec::new();
3619            for (&stream_id, &increment) in &self.flow_control.pending_window_updates {
3620                if increment == 0 {
3621                    written_ids.push(stream_id);
3622                    continue;
3623                }
3624                match serializer::gen_window_update(&mut buf[offset..], stream_id, increment) {
3625                    Ok((_, size)) => {
3626                        offset += size;
3627                        written_ids.push(stream_id);
3628                        incr!(names::h2::FRAMES_TX_WINDOW_UPDATE);
3629                    }
3630                    Err(_) => {
3631                        // Buffer full — stop here, remaining entries stay in the map
3632                        break;
3633                    }
3634                }
3635            }
3636            // Remove only the entries we successfully wrote (or skipped)
3637            for id in written_ids {
3638                self.flow_control.pending_window_updates.remove(&id);
3639            }
3640            if offset > 0 {
3641                kawa.storage.fill(offset);
3642                if self.flush_zero_to_socket() {
3643                    self.expect_write = Some(H2StreamId::Zero);
3644                    // Edge-triggered epoll: ensure pending TLS data gets flushed
3645                    if self.socket.socket_wants_write() {
3646                        self.readiness.event.insert(Ready::WRITABLE);
3647                    }
3648                    return Some(MuxResult::Continue);
3649                }
3650            }
3651        }
3652
3653        // Stage — RST_STREAM cap check + drain.
3654        // Check the lifetime total (not just pending queue length) because
3655        // writable() drains the queue between readable() calls, so the
3656        // pending count alone may never reach the cap even under sustained
3657        // misbehavior.
3658        if !matches!(self.state, H2State::GoAway | H2State::Error)
3659            && self.total_rst_streams_queued >= MAX_PENDING_RST_STREAMS
3660        {
3661            error!(
3662                "{} total RST_STREAM count {} exceeds cap {}, sending GOAWAY(ENHANCE_YOUR_CALM)",
3663                log_context!(self),
3664                self.total_rst_streams_queued,
3665                MAX_PENDING_RST_STREAMS
3666            );
3667            return Some(self.goaway(H2Error::EnhanceYourCalm));
3668        }
3669
3670        // Flush pending RST_STREAM frames (queued when refusing streams).
3671        // Accounting happens at queue-time inside `Self::enqueue_rst`, so
3672        // this drain only serialises and flushes — no metric/flood calls
3673        // here would double-count.
3674        if !self.pending_rst_streams.is_empty() && self.expect_write.is_none() {
3675            let kawa = &mut self.zero;
3676            kawa.storage.clear();
3677            let buf = kawa.storage.space();
3678            let mut offset = 0;
3679            let mut written_count = 0;
3680            for &(stream_id, ref error) in &self.pending_rst_streams {
3681                let frame_size =
3682                    parser::FRAME_HEADER_SIZE + parser::RST_STREAM_PAYLOAD_SIZE as usize;
3683                if offset + frame_size > buf.len() {
3684                    break;
3685                }
3686                match serializer::gen_rst_stream(&mut buf[offset..], stream_id, error.to_owned()) {
3687                    Ok((_, _)) => {
3688                        offset += frame_size;
3689                        written_count += 1;
3690                    }
3691                    Err(_) => break,
3692                }
3693            }
3694            self.pending_rst_streams.drain(..written_count);
3695            if offset > 0 {
3696                kawa.storage.fill(offset);
3697                if self.flush_zero_to_socket() {
3698                    self.expect_write = Some(H2StreamId::Zero);
3699                    // Edge-triggered epoll: ensure pending TLS data gets flushed
3700                    if self.socket.socket_wants_write() {
3701                        self.readiness.event.insert(Ready::WRITABLE);
3702                    }
3703                    return Some(MuxResult::Continue);
3704                }
3705            }
3706        }
3707
3708        None
3709    }
3710
3711    pub fn writable<E, L>(&mut self, context: &mut Context<L>, endpoint: E) -> MuxResult
3712    where
3713        E: Endpoint,
3714        L: ListenerHandler + L7ListenerHandler,
3715    {
3716        self.prune_inactive_streams_while_closing(context);
3717
3718        if let Some(result) = self.flush_pending_control_frames() {
3719            return result;
3720        }
3721
3722        // Flush any pending TLS records before state-specific processing.
3723        // This ensures response DATA frames that were accepted by rustls
3724        // (via socket_write_vectored in write_streams) are pushed to the
3725        // TCP socket even when the connection is in GoAway or Error state.
3726        // Without this, the state-specific handlers may call force_disconnect()
3727        // before the response data reaches the kernel's TCP send buffer.
3728        if self.socket.socket_wants_write() {
3729            self.socket.socket_write(&[]);
3730        }
3731
3732        match (&self.state, &self.position) {
3733            (H2State::Error, Position::Server) => {
3734                if self.socket.socket_wants_write() {
3735                    self.ensure_tls_flushed();
3736                    MuxResult::Continue
3737                } else {
3738                    MuxResult::CloseSession
3739                }
3740            }
3741            (H2State::Error, _)
3742            | (H2State::ClientSettings, Position::Server)
3743            | (H2State::ServerSettings, Position::Client(..)) => {
3744                error!(
3745                    "{} Unexpected combination: (Writable, {:?}, {:?})",
3746                    log_context!(self),
3747                    self.state,
3748                    self.position
3749                );
3750                self.force_disconnect()
3751            }
3752            (H2State::ClientPreface, Position::Server) => MuxResult::Continue,
3753            // Discard state: pending data (e.g. RST_STREAM) was already
3754            // written in the preamble above; let the readable path consume
3755            // the remaining frame payload.
3756            (H2State::Discard, _) => MuxResult::Continue,
3757            (H2State::GoAway, _) => {
3758                if self.peer_gone_after_final_goaway() {
3759                    return MuxResult::CloseSession;
3760                }
3761                // Flush any remaining TLS response data before disconnecting.
3762                // The GoAway state only enters after control frames (our GOAWAY
3763                // response) are flushed above, but response DATA frames may still
3764                // be in rustls's TLS output buffer — accepted by socket_write_vectored
3765                // during write_streams() but not yet flushed to TCP. Under TCP
3766                // backpressure (HAProxy chain), this is the primary truncation vector.
3767                if self.socket.socket_wants_write() {
3768                    self.socket.socket_write(&[]);
3769                    if self.socket.socket_wants_write() {
3770                        // TLS data still pending (TCP backpressure) — don't disconnect
3771                        // yet. Re-arm WRITABLE so the event loop retries the flush.
3772                        self.ensure_tls_flushed();
3773                        return MuxResult::Continue;
3774                    }
3775                }
3776                self.force_disconnect()
3777            }
3778            (H2State::ClientPreface, Position::Client(..)) => {
3779                trace!("{} Preparing preface and settings", log_context!(self));
3780                let pri = serializer::H2_PRI.as_bytes();
3781                let kawa = &mut self.zero;
3782
3783                kawa.storage.space()[0..pri.len()].copy_from_slice(pri);
3784                kawa.storage.fill(pri.len());
3785                match serializer::gen_settings(kawa.storage.space(), &self.local_settings) {
3786                    Ok((_, size)) => {
3787                        kawa.storage.fill(size);
3788                        incr!(names::h2::FRAMES_TX_SETTINGS);
3789                        // RFC 9113 §6.5: start tracking SETTINGS ACK timeout
3790                        self.settings_sent_at = Some(Instant::now());
3791                    }
3792                    Err(error) => {
3793                        error!(
3794                            "{} Could not serialize SettingsFrame: {:?}",
3795                            log_context!(self),
3796                            error
3797                        );
3798                        return self.force_disconnect();
3799                    }
3800                };
3801
3802                self.state = H2State::ClientSettings;
3803                self.expect_write = Some(H2StreamId::Zero);
3804                MuxResult::Continue
3805            }
3806            (H2State::ClientSettings, Position::Client(..)) => {
3807                trace!("{} Sent preface and settings", log_context!(self));
3808                self.state = H2State::ServerSettings;
3809                self.expect_read = Some((H2StreamId::Zero, 9));
3810                self.readiness.interest.remove(Ready::WRITABLE);
3811                MuxResult::Continue
3812            }
3813            (H2State::ServerSettings, Position::Server) => {
3814                // Enlarge the connection-level receive window beyond the RFC default
3815                // of 65 535 bytes. The configured window size is too small for
3816                // high-throughput proxying and causes excessive WINDOW_UPDATE
3817                // round-trips. Use additive increment rather than unconditional
3818                // assignment to preserve any window changes that occurred during
3819                // setup. Skip if the configured window equals the default (no
3820                // enlargement needed), since a zero-increment WINDOW_UPDATE
3821                // violates RFC 9113 §6.9.
3822                let increment = self
3823                    .connection_config
3824                    .initial_connection_window
3825                    .saturating_sub(DEFAULT_INITIAL_WINDOW_SIZE);
3826                if increment > 0 {
3827                    self.queue_window_update(0, increment);
3828                }
3829                // Do NOT increment flow_control.window here: sending our own
3830                // WINDOW_UPDATE enlarges the peer's send allowance, not ours.
3831                // Our send window is only updated by WINDOW_UPDATEs we receive
3832                // from the peer (RFC 9113 §6.9).
3833                self.expect_header();
3834                // Keep WRITABLE so the queued WINDOW_UPDATE gets flushed.
3835                MuxResult::Continue
3836            }
3837            // Proxying states — writing application data (request/response).
3838            // Reset the timeout here, not at the top of writable(), so that
3839            // control frame writes (PING, WINDOW_UPDATE) don't reset it.
3840            (H2State::Header, _)
3841            | (H2State::Frame(_), _)
3842            | (H2State::ContinuationFrame(_), _)
3843            | (H2State::ContinuationHeader(_), _) => self.write_streams(context, endpoint),
3844        }
3845    }
3846
3847    /// Snapshot the access-log RTTs for the local frontend and the linked backend.
3848    ///
3849    /// `Position::Server`-only. On a backend H2 connection (`Position::Client`)
3850    /// the snapshot would write swapped values onto the shared `Stream.metrics`:
3851    /// the connection's `socket` is the upstream and the corresponding
3852    /// `EndpointServer::socket` returns the frontend, so the per-stream
3853    /// `client_rtt`/`server_rtt` cells would be populated with mislabelled
3854    /// values. Gating keeps backend H2 from poisoning the access-log metric
3855    /// for the matching frontend stream.
3856    ///
3857    /// Callers must invoke this BEFORE `endpoint.end_stream(...)` on reset
3858    /// paths so the backend lookup does not depend on
3859    /// `EndpointClient::end_stream` continuing to leave entries in
3860    /// `Router.backends`.
3861    ///
3862    /// Takes individual field references (not `&self`) for the same reason
3863    /// `try_recycle_server_stream` does — to avoid borrow conflicts with the
3864    /// `H2BlockConverter` that holds `&mut self.encoder` during the per-stream
3865    /// write loop.
3866    fn snapshot_rtts<E: Endpoint>(
3867        position: &Position,
3868        socket: &Front,
3869        endpoint: &E,
3870        linked_token: Option<mio::Token>,
3871    ) -> (Option<Duration>, Option<Duration>) {
3872        if !position.is_server() {
3873            return (None, None);
3874        }
3875        (
3876            socket_rtt(socket.socket_ref()),
3877            linked_token
3878                .and_then(|t| endpoint.socket(t))
3879                .and_then(socket_rtt),
3880        )
3881    }
3882
3883    /// Try to recycle a completed server-side stream by distributing overhead,
3884    /// generating access logs, and transitioning the stream to `Recycle` state.
3885    ///
3886    /// Returns `Some((stream_id, Option<token>))` if the stream was recycled, so the
3887    /// caller can add `stream_id` to the dead-streams list and call `endpoint.end_stream()`
3888    /// if a token was returned. Returns `None` if recycling was deferred or not applicable.
3889    ///
3890    /// Takes individual field references instead of `&mut self` to avoid borrow
3891    /// conflicts when the H2 block converter holds `&mut self.encoder`.
3892    /// `client_rtt`/`server_rtt` are snapshotted by the caller (which still
3893    /// owns `&self.socket` and `&endpoint`) and forwarded into the access log.
3894    #[allow(clippy::too_many_arguments)]
3895    fn try_recycle_server_stream<L>(
3896        position: &Position,
3897        bytes: &mut H2ByteAccounting,
3898        streams: &HashMap<StreamId, GlobalStreamId>,
3899        stream: &mut crate::protocol::mux::Stream,
3900        global_stream_id: GlobalStreamId,
3901        stream_id: StreamId,
3902        byte_totals: (usize, usize),
3903        debug: &mut DebugHistory,
3904        listener: std::rc::Rc<std::cell::RefCell<L>>,
3905        client_rtt: Option<Duration>,
3906        server_rtt: Option<Duration>,
3907    ) -> Option<(StreamId, Option<mio::Token>)>
3908    where
3909        L: ListenerHandler + L7ListenerHandler,
3910    {
3911        match position {
3912            Position::Client(..) => None,
3913            Position::Server => {
3914                // Already logged by a reset path; retire the stream after its RST is flushed.
3915                if stream.metrics.start.is_none() {
3916                    let state = std::mem::replace(&mut stream.state, StreamState::Recycle);
3917                    return match state {
3918                        StreamState::Linked(token) => Some((stream_id, Some(token))),
3919                        _ => Some((stream_id, None)),
3920                    };
3921                }
3922
3923                // Don't recycle if the client hasn't sent END_STREAM yet —
3924                // more DATA frames may arrive for this stream.
3925                if !stream.front_received_end_of_stream {
3926                    trace!(
3927                        "{} Defer recycle stream {}: client still sending",
3928                        log_module_context!(),
3929                        global_stream_id
3930                    );
3931                    return None;
3932                }
3933                let stream_bytes = (
3934                    stream.metrics.bin + stream.metrics.backend_bin,
3935                    stream.metrics.bout + stream.metrics.backend_bout,
3936                );
3937                distribute_overhead(
3938                    &mut stream.metrics,
3939                    &mut bytes.overhead_bin,
3940                    &mut bytes.overhead_bout,
3941                    stream_bytes,
3942                    byte_totals,
3943                    streams.len(),
3944                    streams.len() == 1,
3945                );
3946                debug.push(DebugEvent::StreamEvent(4, global_stream_id));
3947                trace!(
3948                    "{} Recycle stream: {}",
3949                    log_module_context!(),
3950                    global_stream_id
3951                );
3952                let token = Self::complete_server_stream(stream, listener, client_rtt, server_rtt);
3953                Some((stream_id, token))
3954            }
3955        }
3956    }
3957
3958    /// Finalize a server-side stream after its response has been fully written.
3959    ///
3960    /// Generates an access log, resets metrics, and transitions the stream to `Recycle`.
3961    /// Returns the backend token if the stream was `Linked`, so the caller can call
3962    /// `endpoint.end_stream()` with the full `Context` (which can't be passed here
3963    /// because `stream` borrows from `context.streams`).
3964    ///
3965    /// Callers must distribute overhead *before* calling this, since the converter
3966    /// borrow may prevent `distribute_overhead()`.
3967    fn complete_server_stream<L>(
3968        stream: &mut crate::protocol::mux::Stream,
3969        listener: std::rc::Rc<std::cell::RefCell<L>>,
3970        client_rtt: Option<Duration>,
3971        server_rtt: Option<Duration>,
3972    ) -> Option<mio::Token>
3973    where
3974        L: ListenerHandler + L7ListenerHandler,
3975    {
3976        incr!(names::http::E2E_H2);
3977        stream.metrics.backend_stop();
3978        stream.generate_access_log(
3979            false,
3980            Some("H2::Complete"),
3981            listener,
3982            client_rtt,
3983            server_rtt,
3984        );
3985        stream.metrics.reset();
3986        let state = std::mem::replace(&mut stream.state, StreamState::Recycle);
3987        if let StreamState::Linked(token) = state {
3988            Some(token)
3989        } else {
3990            None
3991        }
3992    }
3993
3994    /// Compute the total bytes transferred across all active streams.
3995    ///
3996    /// Returns `(total_bytes_in, total_bytes_out)` where bytes_in = `bin + backend_bin`
3997    /// and bytes_out = `bout + backend_bout` for each stream.
3998    fn compute_stream_byte_totals<L: ListenerHandler + L7ListenerHandler>(
3999        &self,
4000        context: &Context<L>,
4001    ) -> (usize, usize) {
4002        let mut total_in = 0usize;
4003        let mut total_out = 0usize;
4004        for &gid in self.streams.values() {
4005            let m = &context.streams[gid].metrics;
4006            total_in += m.bin + m.backend_bin;
4007            total_out += m.bout + m.backend_bout;
4008        }
4009        (total_in, total_out)
4010    }
4011
4012    /// Distribute connection-level byte overhead proportionally to a single stream.
4013    ///
4014    /// `totals` should be pre-computed via [`compute_stream_byte_totals`] **before**
4015    /// taking a mutable borrow on the target stream, to avoid borrow conflicts.
4016    /// Delegates to the free function [`distribute_overhead`].
4017    fn distribute_overhead(&mut self, metrics: &mut SessionMetrics, totals: (usize, usize)) {
4018        let stream_bytes = (
4019            metrics.bin + metrics.backend_bin,
4020            metrics.bout + metrics.backend_bout,
4021        );
4022        distribute_overhead(
4023            metrics,
4024            &mut self.bytes.overhead_bin,
4025            &mut self.bytes.overhead_bout,
4026            stream_bytes,
4027            totals,
4028            self.streams.len(),
4029            self.streams.len() <= 1,
4030        );
4031    }
4032
4033    /// Attribute accumulated `zero_bytes_read` to the stream or to connection overhead.
4034    fn attribute_bytes_to_stream(&mut self, metrics: &mut SessionMetrics) {
4035        self.position
4036            .count_bytes_in(metrics, self.bytes.zero_bytes_read);
4037        self.bytes.zero_bytes_read = 0;
4038    }
4039
4040    fn attribute_bytes_to_overhead(&mut self) {
4041        self.bytes.overhead_bin += self.bytes.zero_bytes_read;
4042        self.bytes.zero_bytes_read = 0;
4043    }
4044
4045    /// Queue a WINDOW_UPDATE, coalescing with any existing entry for the same stream_id.
4046    /// RFC 9113 §6.9.1: window size increment MUST be 1..2^31-1 (0x7FFFFFFF).
4047    ///
4048    /// Always signals pending write so callers don't have to remember the
4049    /// edge-triggered epoll invariant (see memory feedback_epollet_signal_pending_write):
4050    /// under ET epoll a queued WINDOW_UPDATE without a live WRITABLE event bit
4051    /// is invisible to filter_interest() and will never get flushed.
4052    fn queue_window_update(&mut self, stream_id: u32, increment: u32) {
4053        let max_increment = i32::MAX as u32;
4054        if let Some(existing) = self.flow_control.pending_window_updates.get_mut(&stream_id) {
4055            let old = *existing;
4056            *existing = existing.saturating_add(increment).min(max_increment);
4057            // Coalescing invariant: the accumulated increment never decreases
4058            // and never exceeds i32::MAX (RFC 9113 §6.9 caps a WINDOW_UPDATE
4059            // increment at 2^31-1; emitting a larger value would be a protocol
4060            // error on the wire).
4061            debug_assert!(
4062                *existing >= old,
4063                "coalesced WINDOW_UPDATE increment must be monotonic non-decreasing"
4064            );
4065            debug_assert!(
4066                *existing <= max_increment,
4067                "coalesced WINDOW_UPDATE increment must stay within i32::MAX"
4068            );
4069            trace!(
4070                "{} WINDOW_UPDATE coalesced: stream={} old={} new={}",
4071                log_context!(self),
4072                stream_id,
4073                old,
4074                *existing
4075            );
4076        } else if self.flow_control.pending_window_updates.len() < self.max_pending_window_updates {
4077            self.flow_control
4078                .pending_window_updates
4079                .insert(stream_id, increment.min(max_increment));
4080            trace!(
4081                "{} WINDOW_UPDATE queued: stream={} increment={}",
4082                log_context!(self),
4083                stream_id,
4084                increment.min(max_increment)
4085            );
4086        } else {
4087            error!(
4088                "{} WINDOW_UPDATE dropped: queue full ({} entries), stream={} increment={}",
4089                log_context!(self),
4090                self.max_pending_window_updates,
4091                stream_id,
4092                increment
4093            );
4094            incr!(names::h2::WINDOW_UPDATE_DROPPED);
4095        }
4096        self.readiness.arm_writable();
4097    }
4098
4099    /// Re-enable READABLE if this connection is parked waiting for buffer space
4100    /// and the target stream's buffer now has enough room.
4101    ///
4102    /// This is the cross-readiness counterpart to the same-connection check in
4103    /// `writable()`. When the *other side* of a stream (frontend or backend)
4104    /// drains data via its own `writable()`, it frees buffer space that this
4105    /// connection was waiting for. Without this explicit wake-up the connection
4106    /// stays parked and the session deadlocks until a timeout fires.
4107    ///
4108    /// Returns `true` if READABLE was re-enabled.
4109    pub fn try_resume_reading<L>(&mut self, context: &Context<L>) -> bool
4110    where
4111        L: ListenerHandler + L7ListenerHandler,
4112    {
4113        if let Some((
4114            H2StreamId::Other {
4115                gid: global_stream_id,
4116                ..
4117            },
4118            amount,
4119        )) = self.expect_read
4120        {
4121            let stream = &context.streams[global_stream_id];
4122            let kawa = match self.position {
4123                Position::Client(..) => &stream.back,
4124                Position::Server => &stream.front,
4125            };
4126            if kawa.storage.available_space() >= amount {
4127                self.readiness.interest.insert(Ready::READABLE);
4128                return true;
4129            }
4130        }
4131        false
4132    }
4133
4134    /// Mark a stream's position-appropriate end-of-stream flag.
4135    ///
4136    /// Server reads from the front (client), so sets `front_received_end_of_stream`.
4137    /// Client reads from the back (backend), so sets `back_received_end_of_stream`.
4138    fn mark_end_of_stream(&self, stream: &mut crate::protocol::mux::Stream) {
4139        if self.position.is_server() {
4140            stream.front_received_end_of_stream = true;
4141        } else {
4142            stream.back_received_end_of_stream = true;
4143        }
4144    }
4145
4146    /// Cancel streams that have been idle longer than [`Self::stream_idle_timeout`].
4147    ///
4148    /// A stream is considered idle when no meaningful application data (non-empty
4149    /// DATA frames or HEADERS) has been received since the last activity timestamp
4150    /// in [`Self::stream_last_activity_at`].
4151    ///
4152    /// Mitigates slow-multiplex Slowloris (Pass 4 Medium #3): the connection-level
4153    /// idle timer resets on every frame, so a peer sending periodic control frames
4154    /// can pin `max_concurrent_streams` slots for the full nominal connection timeout.
4155    /// Per-stream idle deadlines guarantee each stream terminates if it stops making
4156    /// forward progress, regardless of connection-level liveness.
4157    ///
4158    /// Timed-out streams receive RST_STREAM(CANCEL) and are immediately removed
4159    /// from the streams map so they no longer count against MAX_CONCURRENT_STREAMS.
4160    /// Backend endpoints are notified and metrics are finalized.
4161    pub fn cancel_timed_out_streams<E, L>(&mut self, context: &mut Context<L>, endpoint: &mut E)
4162    where
4163        E: Endpoint,
4164        L: ListenerHandler + L7ListenerHandler,
4165    {
4166        // Per-connection scratch Vecs (`converter_buf`, `lowercase_buf`,
4167        // `cookie_buf`, `priorities_buf`) grow to a
4168        // high-water mark and never shrink. On a long-lived idle H2
4169        // connection that briefly carried a flurry of large headers, the
4170        // backing memory stays pinned indefinitely. Reclaim past
4171        // `SCRATCH_BUF_RETAIN` when the connection has live streams but
4172        // each scratch buffer holds 4× the cap. Quiet-time only — runs
4173        // at the top of every `cancel_timed_out_streams` invocation
4174        // (which is itself called from the readable hot loop, but only
4175        // on a session that has been idle long enough to risk timing
4176        // out a stream).
4177        const SCRATCH_BUF_RETAIN: usize = 16 * 1024;
4178        if self.converter_buf.capacity() > SCRATCH_BUF_RETAIN * 4 {
4179            self.converter_buf.shrink_to(SCRATCH_BUF_RETAIN);
4180        }
4181        if self.lowercase_buf.capacity() > SCRATCH_BUF_RETAIN * 4 {
4182            self.lowercase_buf.shrink_to(SCRATCH_BUF_RETAIN);
4183        }
4184        if self.cookie_buf.capacity() > SCRATCH_BUF_RETAIN * 4 {
4185            self.cookie_buf.shrink_to(SCRATCH_BUF_RETAIN);
4186        }
4187        if self.priorities_buf.capacity() > SCRATCH_BUF_RETAIN * 4 {
4188            self.priorities_buf.shrink_to(SCRATCH_BUF_RETAIN);
4189        }
4190
4191        if self.streams.is_empty()
4192            || (self.stream_last_activity_at.is_empty() && self.stream_fc_stalled_since.is_empty())
4193        {
4194            return;
4195        }
4196        let now = Instant::now();
4197        let deadline = self.stream_idle_timeout;
4198        // Two independent per-stream guards reap on the same deadline — see
4199        // `collect_timed_out_streams`. The flow-control-stall guard
4200        // (`stream_fc_stalled_since`) closes the HTTP/2 window-stall vector that
4201        // the bidirectional liveness guard (`stream_last_activity_at`) misses,
4202        // because an inbound DATA drip keeps the liveness timer warm while the
4203        // response stays window-blocked.
4204        let timed_out = collect_timed_out_streams(
4205            &self.stream_last_activity_at,
4206            &self.stream_fc_stalled_since,
4207            &self.streams,
4208            &self.rst_sent,
4209            now,
4210            deadline,
4211        );
4212        if timed_out.is_empty() {
4213            return;
4214        }
4215        for (sid, reason) in timed_out {
4216            info!(
4217                "{} H2 stream {} exceeded {:?} ({}), cancelling",
4218                log_context!(self),
4219                sid,
4220                deadline,
4221                reason
4222            );
4223            // M1: break reaps down by guard so a window-stall reap (a DoS
4224            // mitigation) is distinguishable from an ordinary idle reap on a
4225            // dashboard. M2: a window-stall reap whose stream dribbled some
4226            // outbound progress (`acc > 0`) below the floor is specifically a
4227            // stall-budget reap — the `WINDOW_UPDATE`-drip vector the budget
4228            // closes — counted as a subset. Read the accumulator BEFORE
4229            // `remove_dead_stream` evicts it below.
4230            match reason {
4231                "H2::WindowStall" => {
4232                    count!(names::h2::STREAMS_REAPED_WINDOW_STALL, 1);
4233                    if matches!(self.stream_fc_stalled_progress.get(&sid), Some(&acc) if acc > 0) {
4234                        count!(names::h2::STREAMS_REAPED_STALL_BUDGET, 1);
4235                    }
4236                }
4237                "H2::IdleTimeout" => count!(names::h2::STREAMS_REAPED_IDLE_TIMEOUT, 1),
4238                other => debug!("{} unexpected reap reason {}", log_context!(self), other),
4239            }
4240            // Route through the canonical chokepoint so dedupe (rst_sent),
4241            // queued-cap accounting (MAX_PENDING_RST_STREAMS via
4242            // total_rst_streams_queued), and edge-triggered-epoll arming
4243            // (Readiness::arm_writable) all stay consistent — see LIFECYCLE
4244            // §8.2. The previous direct push bypassed all three: a peer
4245            // that opens 200 streams and lets them all idle past
4246            // stream_idle_timeout could push past the queued cap silently
4247            // (no GOAWAY(ENHANCE_YOUR_CALM) escalation), a double-cancel
4248            // pass would grow pending_rst_streams instead of short-
4249            // circuiting on the existing rst_sent membership, and the
4250            // hand-rolled `interest.insert(WRITABLE) + signal_pending_write`
4251            // pair below skipped invariant 15. Counting these RSTs against
4252            // the cap is a deliberate behaviour change: 200 cumulative idle
4253            // cancellations from one peer IS abusive (pinning
4254            // MAX_CONCURRENT_STREAMS slots), and the GOAWAY(ENHANCE_YOUR_CALM)
4255            // escalation tells the peer to reconnect with a clean state.
4256            //
4257            // We deliberately ignore the `Option<MuxResult>` flood-violation
4258            // signal here — `cancel_timed_out_streams` returns `()` and is
4259            // called as best-effort housekeeping during the read path. A
4260            // flood violation that becomes visible mid-iteration will be
4261            // re-detected on the next `record_rst_emitted` call (the
4262            // counter is sticky), so dropping the early-return is safe.
4263            let _ = self.enqueue_rst(sid, H2Error::Cancel);
4264
4265            // Remove from streams map and recycle the context stream so the slot
4266            // no longer counts against MAX_CONCURRENT_STREAMS.
4267            // Compute totals per-stream before remove (matches RST_STREAM handler).
4268            let byte_totals = self.compute_stream_byte_totals(context);
4269            if let Some(global_stream_id) = self.streams.get(&sid).copied() {
4270                {
4271                    let stream = &mut context.streams[global_stream_id];
4272                    self.attribute_bytes_to_stream(&mut stream.metrics);
4273                }
4274                // Check if stream is linked to a backend — borrow must be scoped
4275                // so end_stream can take &mut context.
4276                let linked_token = context.streams[global_stream_id].linked_token();
4277                let (client_rtt, server_rtt) =
4278                    Self::snapshot_rtts(&self.position, &self.socket, &*endpoint, linked_token);
4279                if let Some(token) = linked_token {
4280                    endpoint.end_stream(token, global_stream_id, context);
4281                }
4282                let stream = &mut context.streams[global_stream_id];
4283                match &self.position {
4284                    Position::Client(_, backend, BackendStatus::Connected) => {
4285                        let mut backend_borrow = backend.borrow_mut();
4286                        backend_borrow.active_requests =
4287                            backend_borrow.active_requests.saturating_sub(1);
4288                    }
4289                    Position::Client(..) => {}
4290                    Position::Server => {
4291                        self.distribute_overhead(&mut stream.metrics, byte_totals);
4292                        stream.metrics.backend_stop();
4293                        stream.generate_access_log(
4294                            true,
4295                            Some(reason),
4296                            context.listener.clone(),
4297                            client_rtt,
4298                            server_rtt,
4299                        );
4300                        stream.state = StreamState::Recycle;
4301                    }
4302                }
4303                // Retire sid from streams/prioriser/stream_last_activity_at and
4304                // invalidate expect_write/expect_read if they reference this gid.
4305                self.remove_dead_stream(sid, global_stream_id);
4306            }
4307        }
4308        // Writable arming is already done by enqueue_rst -> arm_writable in
4309        // the loop above; the trailing pair was redundant after the chokepoint
4310        // routing landed.
4311    }
4312
4313    /// Queue a `RST_STREAM` frame for serialisation by
4314    /// [`Self::flush_pending_control_frames`] on the next writable tick.
4315    ///
4316    /// This is the canonical entry point for proxy-emitted stream resets:
4317    /// `DATA` on a closed stream, `MAX_CONCURRENT_STREAMS` refusal, and the
4318    /// per-stream error paths in [`Self::reset_stream`] all funnel through
4319    /// here. Serialisation is independent of the owning `Stream` still
4320    /// existing in `self.streams`, which is what lets us emit even after a
4321    /// caller has already called [`Self::remove_dead_stream`].
4322    ///
4323    /// Delegates the primitive work to [`enqueue_rst_into`] so the invariants
4324    /// are covered by unit tests that don't need a full `ConnectionH2`
4325    /// fixture. See that function's doc-comment for the three invariants
4326    /// (dedupe via `rst_sent`, MadeYouReset queued cap via
4327    /// `total_rst_streams_queued`, edge-triggered-epoll arm via
4328    /// [`Readiness::arm_writable`]).
4329    fn enqueue_rst(&mut self, wire_stream_id: StreamId, error: H2Error) -> Option<MuxResult> {
4330        let freshly_queued = enqueue_rst_into(
4331            &mut self.pending_rst_streams,
4332            &mut self.total_rst_streams_queued,
4333            &mut self.rst_sent,
4334            &mut self.readiness,
4335            wire_stream_id,
4336            error,
4337        );
4338        // Account ONLY when a new RST actually entered the queue.
4339        // Calling `enqueue_rst` for a stream that already has a queued
4340        // (or already-flushed) RST is the dedup short-circuit — counting
4341        // those would inflate `h2.frames.tx.rst_stream` /
4342        // `h2.rst_stream.sent.*` and trip the CVE-2025-8671 MadeYouReset
4343        // lifetime cap on frames that never reached the wire.
4344        //
4345        // Account at queue-time, not at drain-time. Doing it later in
4346        // `flush_pending_control_frames` would double-count any RST that
4347        // a re-entrant call (DATA on a closed stream we already RSTed)
4348        // tried to enqueue — and missing it at queue-time leaves
4349        // `cancel_timed_out_streams` / `refuse_stream_and_discard` /
4350        // DATA-on-closed-stream paths bypassing the lifetime cap
4351        // (security review LISA-001 on commit `da845c71`).
4352        if freshly_queued {
4353            self.account_emitted_rst(error)
4354        } else {
4355            None
4356        }
4357    }
4358
4359    /// Single accounting site for proxy-emitted RST_STREAM frames.
4360    /// Three things must happen for every emitted RST so flood-protection
4361    /// stays honest: the global tx counter, the per-error breakdown,
4362    /// and the MadeYouReset emitted-RST lifetime cap.
4363    ///
4364    /// Two distinct emission paths feed this helper:
4365    ///   * Queued frames — [`Self::enqueue_rst`] (and therefore every
4366    ///     callable that funnels through it: `reset_stream`,
4367    ///     `refuse_stream_and_discard`, `cancel_timed_out_streams`,
4368    ///     DATA-on-closed-stream) calls this once at queue-time. The
4369    ///     drain in `flush_pending_control_frames` does NOT call it
4370    ///     again — that would double-count.
4371    ///   * Converter-emitted frames — the converter's `initialize`
4372    ///     chokepoint (and the HPACK over-budget abort path) writes
4373    ///     RST_STREAM frames straight into `kawa.out` from inside
4374    ///     `kawa.prepare`. We collect those `H2Error` codes during the
4375    ///     `write_streams` loop and call this helper for each one
4376    ///     after `drop(converter)` (because the converter holds
4377    ///     `&mut self.encoder`).
4378    ///
4379    /// Returning `Some(MuxResult)` means the caller MUST short-circuit
4380    /// with that result — the flood detector tripped its lifetime cap
4381    /// and converted to a connection-wide GOAWAY.
4382    fn account_emitted_rst(&mut self, error: H2Error) -> Option<MuxResult> {
4383        incr!(names::h2::FRAMES_TX_RST_STREAM);
4384        count!(metric_for_rst_stream_sent(error), 1);
4385        if !matches!(error, H2Error::NoError)
4386            && let Some(violation) = self.flood_detector.record_rst_emitted()
4387        {
4388            return Some(self.handle_flood_violation(violation));
4389        }
4390        None
4391    }
4392
4393    /// Refuse a newly-opened stream with RST_STREAM and discard its HEADERS payload.
4394    ///
4395    /// Used when MAX_CONCURRENT_STREAMS is exceeded or buffer pool is exhausted.
4396    /// Queues the RST_STREAM for the writable path (can't write to kawa.storage
4397    /// here because it is needed to discard the HEADERS payload).
4398    ///
4399    /// Also applies SETTINGS back-pressure per RFC 9113 §5.1.2: if refusals
4400    /// burst past [`BACKPRESSURE_REFUSAL_THRESHOLD`] within
4401    /// [`BACKPRESSURE_WINDOW_DURATION`], the advertised
4402    /// `SETTINGS_MAX_CONCURRENT_STREAMS` is halved via
4403    /// [`Self::apply_mcs_backpressure`].
4404    fn refuse_stream_and_discard(
4405        &mut self,
4406        stream_id: StreamId,
4407        error: H2Error,
4408        payload_len: u32,
4409    ) -> MuxResult {
4410        if let Some(result) = self.enqueue_rst(stream_id, error) {
4411            return result;
4412        }
4413        self.state = H2State::Discard;
4414        self.expect_read = Some((H2StreamId::Zero, payload_len as usize));
4415        self.record_refusal_for_backpressure();
4416        MuxResult::Continue
4417    }
4418
4419    /// RFC 9113 §5.1.2 SETTINGS back-pressure bookkeeping.
4420    ///
4421    /// Increments the refusal counter for the current back-pressure window
4422    /// and, when the burst threshold is crossed, halves the advertised
4423    /// `SETTINGS_MAX_CONCURRENT_STREAMS`. Further halving attempts in the
4424    /// same connection are suppressed by [`Self::mcs_backpressure_applied`]
4425    /// so sustained abuse does not collapse the cap to zero — callers can
4426    /// still promote the situation to `EnhanceYourCalm` via the flood
4427    /// detector.
4428    fn record_refusal_for_backpressure(&mut self) {
4429        if self.refuse_window_start.elapsed() >= BACKPRESSURE_WINDOW_DURATION {
4430            self.refuse_count_window = 0;
4431            self.refuse_window_start = Instant::now();
4432        }
4433        self.refuse_count_window = self.refuse_count_window.saturating_add(1);
4434        if !self.mcs_backpressure_applied
4435            && self.refuse_count_window >= BACKPRESSURE_REFUSAL_THRESHOLD
4436        {
4437            self.apply_mcs_backpressure();
4438        }
4439    }
4440
4441    /// Halve the advertised `SETTINGS_MAX_CONCURRENT_STREAMS` and mark the
4442    /// back-pressure state as applied. The new value takes effect locally
4443    /// immediately — subsequent stream-open checks in `handle_header_state`
4444    /// compare `self.streams.len()` against this reduced cap, so the peer
4445    /// starts receiving `REFUSED_STREAM` earlier. A full SETTINGS re-send on
4446    /// the wire is deferred until we have a mid-connection SETTINGS queue
4447    /// (the existing path in `handle_preface_state` only fires during the
4448    /// handshake); this is noted in the task log as a minimal first step.
4449    fn apply_mcs_backpressure(&mut self) {
4450        let previous = self.local_settings.settings_max_concurrent_streams;
4451        let reduced = (previous / 2).max(1);
4452        warn!(
4453            "{} H2 SETTINGS back-pressure: refusals={} in {}s — halving \
4454             SETTINGS_MAX_CONCURRENT_STREAMS {} -> {}",
4455            log_context!(self),
4456            self.refuse_count_window,
4457            BACKPRESSURE_WINDOW_DURATION.as_secs(),
4458            previous,
4459            reduced,
4460        );
4461        self.local_settings.settings_max_concurrent_streams = reduced;
4462        self.mcs_backpressure_applied = true;
4463    }
4464
4465    /// Log a flood violation with full session context and emit the GOAWAY.
4466    ///
4467    /// Centralises the "flood detected" reporting so every site that observes a
4468    /// [`H2FloodViolation`] gets the same session-scoped log line, matching the
4469    /// RUSTLS log-context convention. Also emits the per-kind statsd counter
4470    /// (`h2.flood.violation.<kind>`) so SOC dashboards can window the trip
4471    /// rate without parsing logs — every CVE-mitigation in the H2 family
4472    /// (Rapid Reset, MadeYouReset, CONTINUATION/PING/SETTINGS floods, header
4473    /// overflow, glitch) funnels through this site.
4474    pub fn handle_flood_violation(&mut self, violation: H2FloodViolation) -> MuxResult {
4475        count!(violation.metric_key, 1);
4476        warn!(
4477            "{} H2 flood detected: {} count {} exceeds threshold {}",
4478            log_context!(self),
4479            violation.reason,
4480            violation.count,
4481            violation.threshold,
4482        );
4483        self.goaway(violation.error)
4484    }
4485}
4486
4487/// Recover the [`H2Error`] code that the converter's `initialize`
4488/// chokepoint will encode into the synthesised RST_STREAM frame for a
4489/// kawa stuck in [`kawa::ParsingPhase::Error`]. Mirrors the parse +
4490/// fallback at `lib/src/protocol/mux/converter.rs::initialize` so the
4491/// flood-accounting helper sees the same code that lands on the wire.
4492fn rst_error_from_kawa<T: kawa::AsBuffer>(kawa: &kawa::Kawa<T>) -> H2Error {
4493    match kawa.parsing_phase {
4494        kawa::ParsingPhase::Error {
4495            kind: kawa::ParsingErrorKind::Processing { message },
4496            ..
4497        } => message.parse::<H2Error>().unwrap_or(H2Error::InternalError),
4498        _ => H2Error::InternalError,
4499    }
4500}
4501
4502/// Compile-time mapping from `(prefix, H2Error)` to a static metric key.
4503///
4504/// Materialises a `&'static str` literal via `concat!`, so the metric key
4505/// never crosses through a heap allocation and the statsd drain can store it
4506/// as `&'static str`. Adding a new `H2Error` variant fails the build here —
4507/// the metric breakdown stays in lock-step with RFC 9113 §7 codes.
4508///
4509/// Used for the per-error-code counters emitted around GOAWAY and RST_STREAM
4510/// in either direction (see `metric_for_goaway_sent` etc. below).
4511macro_rules! h2_error_metric_key {
4512    ($prefix:literal, $error:expr) => {
4513        match $error {
4514            H2Error::NoError => concat!($prefix, ".no_error"),
4515            H2Error::ProtocolError => concat!($prefix, ".protocol_error"),
4516            H2Error::InternalError => concat!($prefix, ".internal_error"),
4517            H2Error::FlowControlError => concat!($prefix, ".flow_control_error"),
4518            H2Error::SettingsTimeout => concat!($prefix, ".settings_timeout"),
4519            H2Error::StreamClosed => concat!($prefix, ".stream_closed"),
4520            H2Error::FrameSizeError => concat!($prefix, ".frame_size_error"),
4521            H2Error::RefusedStream => concat!($prefix, ".refused_stream"),
4522            H2Error::Cancel => concat!($prefix, ".cancel"),
4523            H2Error::CompressionError => concat!($prefix, ".compression_error"),
4524            H2Error::ConnectError => concat!($prefix, ".connect_error"),
4525            H2Error::EnhanceYourCalm => concat!($prefix, ".enhance_your_calm"),
4526            H2Error::InadequateSecurity => concat!($prefix, ".inadequate_security"),
4527            H2Error::HTTP11Required => concat!($prefix, ".http_1_1_required"),
4528        }
4529    };
4530}
4531
4532/// Static metric key for an outbound GOAWAY. Same call shape as the other three
4533/// helpers below — keeps the call sites uniform.
4534fn metric_for_goaway_sent(error: H2Error) -> &'static str {
4535    h2_error_metric_key!("h2.goaway.sent", error)
4536}
4537
4538/// Static metric key for an inbound GOAWAY by raw wire error code. Codes
4539/// outside RFC 9113 §7 fall into the dedicated `…unknown_error` bucket so the
4540/// breakdown stays bounded and operators can still spot non-standard peers.
4541fn metric_for_goaway_received(error_code: u32) -> &'static str {
4542    H2Error::try_from(error_code)
4543        .map(|e| h2_error_metric_key!("h2.goaway.received", e))
4544        .unwrap_or("h2.goaway.received.unknown_error")
4545}
4546
4547/// Static metric key for an outbound RST_STREAM. Mirrors
4548/// [`metric_for_goaway_sent`] under a separate namespace so RST and GOAWAY
4549/// rates can be alerted on independently.
4550fn metric_for_rst_stream_sent(error: H2Error) -> &'static str {
4551    h2_error_metric_key!("h2.rst_stream.sent", error)
4552}
4553
4554/// Static metric key for an inbound RST_STREAM by raw wire error code. Same
4555/// `…unknown_error` fallback as [`metric_for_goaway_received`].
4556fn metric_for_rst_stream_received(error_code: u32) -> &'static str {
4557    H2Error::try_from(error_code)
4558        .map(|e| h2_error_metric_key!("h2.rst_stream.received", e))
4559        .unwrap_or("h2.rst_stream.received.unknown_error")
4560}
4561
4562/// Static metric key for an inbound H2 frame by RFC 9113 §6 frame type.
4563/// Emitted at the `handle_frame` dispatch — single chokepoint that any
4564/// new H2 frame type must traverse, so adding a `Frame::*` variant fails
4565/// the build here. Counts are per-frame, not per-byte; pair with
4566/// `bytes_in` for traffic-mix dashboards.
4567fn h2_frame_rx_metric_key(frame: &Frame) -> &'static str {
4568    match frame {
4569        Frame::Data(_) => "h2.frames.rx.data",
4570        Frame::Headers(_) => "h2.frames.rx.headers",
4571        Frame::PushPromise(_) => "h2.frames.rx.push_promise",
4572        Frame::Priority(_) => "h2.frames.rx.priority",
4573        Frame::RstStream(_) => "h2.frames.rx.rst_stream",
4574        Frame::Settings(_) => "h2.frames.rx.settings",
4575        Frame::Ping(_) => "h2.frames.rx.ping",
4576        Frame::GoAway(_) => "h2.frames.rx.goaway",
4577        Frame::WindowUpdate(_) => "h2.frames.rx.window_update",
4578        Frame::Continuation(_) => "h2.frames.rx.continuation",
4579        Frame::PriorityUpdate(_) => "h2.frames.rx.priority_update",
4580        Frame::Unknown(_) => "h2.frames.rx.unknown",
4581    }
4582}
4583
4584impl<Front: SocketHandler> ConnectionH2<Front> {
4585    pub fn goaway(&mut self, error: H2Error) -> MuxResult {
4586        self.state = H2State::Error;
4587        self.drain.draining = true;
4588        self.expect_read = None;
4589        // Disarm the SETTINGS ACK timer: once we've committed to GOAWAY, the
4590        // timeout check at `readable()` / `flush_pending_control_frames()` must
4591        // not re-fire. Without this, `signal_pending_write()` below re-enters
4592        // `writable()` → `flush_pending_control_frames()` on the next tick,
4593        // the elapsed check is still true, and we emit another
4594        // `warn!` + `goaway()` pair, each bumping `h2.goaway.sent.*`.
4595        self.settings_sent_at = None;
4596        let kawa = &mut self.zero;
4597        kawa.storage.clear();
4598        // Severity tiering: only `InternalError` implies a sozu-side bug when
4599        // WE emit it. Every other non-`NoError` reason is "peer misbehaved,
4600        // sozu defended correctly" — operators don't need paging on abusive
4601        // or buggy peers. Caller sites already log the specific antecedent
4602        // (flood detected, parser failure, SETTINGS timeout, invalid window)
4603        // before reaching `goaway()`, so demoting this summary line avoids
4604        // duplicate noise without hiding the root cause.
4605        match error {
4606            H2Error::NoError => debug!("{} GOAWAY: {:?}", log_context!(self), error),
4607            H2Error::InternalError => error!("{} GOAWAY: {:?}", log_context!(self), error),
4608            _ => warn!("{} GOAWAY: {:?}", log_context!(self), error),
4609        }
4610        count!(metric_for_goaway_sent(error), 1);
4611
4612        // RFC 9113 §6.8: last_stream_id is the highest peer-initiated stream we processed
4613        match serializer::gen_goaway(kawa.storage.space(), self.highest_peer_stream_id, error) {
4614            Ok((_, size)) => {
4615                kawa.storage.fill(size);
4616                incr!(names::h2::FRAMES_TX_GOAWAY);
4617                self.state = H2State::GoAway;
4618                self.expect_write = Some(H2StreamId::Zero);
4619                self.readiness.interest = Ready::WRITABLE | Ready::HUP | Ready::ERROR;
4620                self.readiness.signal_pending_write();
4621                MuxResult::Continue
4622            }
4623            Err(error) => {
4624                error!(
4625                    "{} Could not serialize GoAwayFrame: {:?}",
4626                    log_context!(self),
4627                    error
4628                );
4629                self.force_disconnect()
4630            }
4631        }
4632    }
4633
4634    /// RFC 9113 §6.8: Initiate graceful shutdown using the double-GOAWAY pattern.
4635    ///
4636    /// First call sends GOAWAY with `last_stream_id = 0x7FFFFFFF` (MAX) to signal
4637    /// the intent to stop accepting new streams while allowing in-flight streams
4638    /// to complete. The connection enters draining mode.
4639    ///
4640    /// When `draining` is already true (second invocation), sends the final GOAWAY
4641    /// with the actual `highest_peer_stream_id` so the peer knows which streams
4642    /// were processed.
4643    pub fn graceful_goaway(&mut self) -> MuxResult {
4644        if self.drain.draining {
4645            // Second GOAWAY: send with the real last_stream_id
4646            return self.goaway(H2Error::NoError);
4647        }
4648
4649        // First GOAWAY: advertise MAX stream ID so the peer knows we are draining
4650        // but does not yet know the cutoff. This gives in-flight requests a chance
4651        // to arrive before we commit to a final last_stream_id.
4652        self.drain.draining = true;
4653        // Arm the forced-close timer from the moment the proxy decides to drain.
4654        // `Mux::shutting_down` samples it against `graceful_shutdown_deadline`
4655        // and returns `true` once the budget is exhausted so the session loop
4656        // tears the connection down instead of waiting forever.
4657        self.drain.started_at = Some(Instant::now());
4658        // Keep expect_read as-is: existing streams should continue reading
4659        // data during the drain window opened by the initial GOAWAY. Only
4660        // the final GOAWAY (via `goaway()`) removes READABLE.
4661        let kawa = &mut self.zero;
4662        kawa.storage.clear();
4663        debug!(
4664            "{} GOAWAY (graceful, initial): last_stream_id=0x7FFFFFFF",
4665            log_context!(self)
4666        );
4667        // The initial GOAWAY sends NO_ERROR on the wire — count it under
4668        // the same per-code key as the final GOAWAY. The downstream alert
4669        // that wants to distinguish drain from termination compares
4670        // against the `h2.goaway.sent.no_error` rate (drain) vs the other
4671        // variants (termination on error).
4672        count!(metric_for_goaway_sent(H2Error::NoError), 1);
4673
4674        match serializer::gen_goaway(kawa.storage.space(), STREAM_ID_MAX, H2Error::NoError) {
4675            Ok((_, size)) => {
4676                kawa.storage.fill(size);
4677                incr!(names::h2::FRAMES_TX_GOAWAY);
4678                // Stay in the current state so the connection can continue processing
4679                // existing streams. The final GOAWAY will transition to GoAway state.
4680                // Keep READABLE so in-flight request bodies can still be received
4681                // during the drain window. Only remove READABLE in the final GOAWAY
4682                // (via `goaway()`).
4683                self.expect_write = Some(H2StreamId::Zero);
4684                self.readiness.arm_writable();
4685                MuxResult::Continue
4686            }
4687            Err(error) => {
4688                error!(
4689                    "{} Could not serialize graceful GoAwayFrame: {:?}",
4690                    log_context!(self),
4691                    error
4692                );
4693                self.force_disconnect()
4694            }
4695        }
4696    }
4697
4698    /// Returns `true` when the graceful-shutdown budget armed by
4699    /// [`Self::graceful_goaway`] has elapsed. A return of `true` signals
4700    /// the enclosing session loop that the proxy-initiated drain must
4701    /// transition to a forced close: remaining streams will not complete
4702    /// in time and keeping the connection open past the deadline defeats
4703    /// the soft-stop SLA.
4704    ///
4705    /// Returns `false` when:
4706    /// - drain has not started yet (`started_at` is `None`),
4707    /// - the knob is `0` / `None` (indefinite wait explicitly opted in),
4708    /// - or the elapsed time is still within the configured budget.
4709    pub fn graceful_shutdown_deadline_elapsed(&self) -> bool {
4710        match (self.drain.started_at, self.drain.graceful_shutdown_deadline) {
4711            (Some(started_at), Some(deadline)) => started_at.elapsed() >= deadline,
4712            _ => false,
4713        }
4714    }
4715
4716    /// Returns `true` if there is data queued waiting to be flushed:
4717    /// - H2 control frames in the zero buffer (GOAWAY, SETTINGS ACK, etc.)
4718    /// - A partially-written stream or control frame (`expect_write`)
4719    /// - Encrypted TLS records in rustls's output buffer not yet flushed to TCP
4720    ///
4721    /// The TLS check is critical: `shutting_down()` uses this to prevent
4722    /// premature session close while response DATA is still in rustls's
4723    /// buffer (accepted by `socket_write_vectored` but not yet on the wire).
4724    ///
4725    /// Does NOT check per-stream `back.out`/`back.blocks`; use
4726    /// [`Self::has_pending_write_full`] on paths that must honour
4727    /// LIFECYCLE invariant 16 (e.g. shutdown-drain).
4728    pub fn has_pending_write(&self) -> bool {
4729        if self.peer_gone_after_final_goaway() {
4730            return false;
4731        }
4732        self.expect_write.is_some()
4733            || !self.zero.storage.is_empty()
4734            || self.socket.socket_wants_write()
4735    }
4736
4737    /// True when the reaper has queued control frames (`RST_STREAM`) into
4738    /// `pending_rst_streams` that have not yet been serialized. Kept SEPARATE
4739    /// from [`Self::has_pending_write`] because that probe gates connection close
4740    /// (the `mod.rs` close-gating sites) and must NOT treat a queued RST as a
4741    /// reason to keep the connection open; this probe is consulted ONLY by the
4742    /// `MuxState::timeout` flush gate to push a silent-peer `RST_STREAM(CANCEL)`
4743    /// onto the wire before the connection closes.
4744    pub fn has_pending_control_write(&self) -> bool {
4745        !self.pending_rst_streams.is_empty()
4746    }
4747
4748    /// Connection-level [`Self::has_pending_write`] extended with a per-stream
4749    /// back-buffer probe (LIFECYCLE §9 invariant 16). Used by shutdown-drain
4750    /// paths that must not close while any open stream still has outbound
4751    /// kawa bytes queued — a voluntary scheduler yield can leave `back.out`
4752    /// or `back.blocks` non-empty without `expect_write` being set.
4753    pub fn has_pending_write_full<L>(&self, context: &Context<L>) -> bool
4754    where
4755        L: ListenerHandler + L7ListenerHandler,
4756    {
4757        self.has_pending_write() || any_stream_has_pending_back(&self.streams, &context.streams)
4758    }
4759
4760    /// Flush the zero buffer to the socket, counting bytes as connection overhead.
4761    ///
4762    /// Returns `true` if the socket stalled (WouldBlock / zero-length write),
4763    /// meaning the caller should stop writing and wait for the next writable event.
4764    /// Returns `false` when the buffer has been fully drained.
4765    fn flush_zero_to_socket(&mut self) -> bool {
4766        while !self.zero.storage.is_empty() {
4767            let (size, status) = self.socket.socket_write(self.zero.storage.data());
4768            #[cfg(debug_assertions)]
4769            trace!(
4770                "{} flush_zero_to_socket: written={}, status={:?}, wants_write={}",
4771                log_context!(self),
4772                size,
4773                status,
4774                self.socket.socket_wants_write()
4775            );
4776            self.zero.storage.consume(size);
4777            self.position.count_bytes_out_counter(size);
4778            self.bytes.overhead_bout += size;
4779            if update_readiness_after_write(size, status, &mut self.readiness) {
4780                return true;
4781            }
4782        }
4783        // Reset buffer positions after draining. consume() advances start but
4784        // never resets it, so without clear() the next fill would panic.
4785        self.zero.storage.clear();
4786        false
4787    }
4788
4789    /// Directly flush the zero buffer to the socket without going through
4790    /// the full writable() path. Used during shutdown when the event loop
4791    /// won't deliver new epoll events for this session (edge-triggered).
4792    pub fn flush_zero_buffer(&mut self) {
4793        if self.flush_zero_to_socket() {
4794            return;
4795        }
4796        self.expect_write = None;
4797        if self.socket.socket_wants_write() {
4798            let (_size, status) = self.socket.socket_write(&[]);
4799            let _ = update_readiness_after_write(0, status, &mut self.readiness);
4800        }
4801    }
4802
4803    pub fn create_stream<L>(
4804        &mut self,
4805        stream_id: StreamId,
4806        context: &mut Context<L>,
4807    ) -> Option<GlobalStreamId>
4808    where
4809        L: ListenerHandler + L7ListenerHandler,
4810    {
4811        // RFC 9113 §6.8: reject new streams on a draining connection
4812        if self.drain.draining {
4813            error!(
4814                "{} Rejecting new stream {} on draining connection",
4815                log_context!(self),
4816                stream_id
4817            );
4818            return None;
4819        }
4820        let highest_before = self.highest_peer_stream_id;
4821        let streams_before = self.streams.len();
4822        // Track the highest peer-initiated stream ID for GoAway frames
4823        // before any early return, so GoAway always reports the correct last stream.
4824        if stream_id > self.highest_peer_stream_id {
4825            self.highest_peer_stream_id = stream_id;
4826        }
4827        // highest_peer_stream_id is monotonic non-decreasing — it only ever
4828        // climbs to the largest id we have accepted (RFC 9113 §6.8 last-stream
4829        // reporting depends on this).
4830        debug_assert!(
4831            self.highest_peer_stream_id >= highest_before,
4832            "highest_peer_stream_id must never regress"
4833        );
4834        let global_stream_id = context.create_stream(
4835            Ulid::generate(),
4836            self.peer_settings.settings_initial_window_size,
4837        )?;
4838        self.last_stream_id = (stream_id + 2) & !1;
4839        self.streams.insert(stream_id, global_stream_id);
4840        self.stream_last_activity_at
4841            .insert(stream_id, Instant::now());
4842        // Post-conditions: the stream is now reachable in both indices, the
4843        // active count grew by exactly one (the id was not already present —
4844        // `handle_header_state` rejects re-used ids), and `last_stream_id` is
4845        // the even watermark just past this id so `new_stream_id` never collides.
4846        debug_assert_eq!(
4847            self.streams.get(&stream_id).copied(),
4848            Some(global_stream_id),
4849            "create_stream must register the wire->global mapping"
4850        );
4851        debug_assert!(
4852            self.stream_last_activity_at.contains_key(&stream_id),
4853            "create_stream must arm the per-stream idle timer"
4854        );
4855        debug_assert_eq!(
4856            self.streams.len(),
4857            streams_before + 1,
4858            "create_stream must add exactly one stream (id must not pre-exist)"
4859        );
4860        debug_assert!(
4861            self.last_stream_id > stream_id && self.last_stream_id & 1 == 0,
4862            "last_stream_id watermark must be the even value strictly above stream_id"
4863        );
4864        Some(global_stream_id)
4865    }
4866
4867    pub fn new_stream_id(&mut self) -> Option<StreamId> {
4868        let watermark_before = self.last_stream_id;
4869        let (issued, next) = next_stream_id(self.last_stream_id, self.position.is_client())?;
4870        self.last_stream_id = next;
4871        // Post-conditions: the locally-issued id has the parity of our role and
4872        // the watermark advanced strictly (so the next allocation cannot reuse
4873        // this id). `next_stream_id` already asserts parity vs `is_client`; here
4874        // we re-assert against `self.position` and the watermark monotonicity.
4875        debug_assert_eq!(
4876            issued & 1 == 1,
4877            self.position.is_client(),
4878            "locally-issued stream id parity must match our role"
4879        );
4880        debug_assert!(
4881            self.last_stream_id > watermark_before,
4882            "issuing a stream id must advance the watermark"
4883        );
4884        Some(issued)
4885    }
4886
4887    /// Test-only setter: jump `last_stream_id` close to [`STREAM_ID_MAX`] so
4888    /// that the next call to [`Self::new_stream_id`] exhausts the 31-bit
4889    /// space. FIX-22 ("Stream-ID exhaustion disconnects backend gracefully")
4890    /// exercises the `None`-return branch — reaching it through normal API
4891    /// usage would require issuing ~2³¹ requests, which is not tractable in
4892    /// an E2E harness.
4893    #[cfg(any(test, feature = "e2e-hooks"))]
4894    pub fn __test_set_last_stream_id(&mut self, id: StreamId) {
4895        self.last_stream_id = id;
4896    }
4897
4898    /// Cross-field invariant sweep for the H2 connection state machine,
4899    /// asserted as a run-to-completion post-condition at the end of every
4900    /// frame-handling pass (see the call in [`Self::handle_frame`]).
4901    ///
4902    /// These are relationships between *separate* fields that no single setter
4903    /// can guarantee on its own — exactly the class of bug TigerStyle's
4904    /// `check_invariants` targets. Each one is cheap (counter compares + a few
4905    /// `HashMap` membership probes); the whole function is `#[cfg(debug_assertions)]`
4906    /// and compiles out of release entirely.
4907    ///
4908    /// Encoded invariants:
4909    /// 1. **Stream-id watermark parity**: locally-issued ids never exceed
4910    ///    `STREAM_ID_MAX`; `last_stream_id` stays the even watermark (it is
4911    ///    rounded to `(id + 2) & !1` and initialised to 0).
4912    /// 2. **Per-stream caches are subsets of the live stream set**:
4913    ///    `stream_last_activity_at` is keyed only by currently-tracked stream
4914    ///    ids — a leak here would let a removed stream keep an idle timer and
4915    ///    mis-fire `cancel_timed_out_streams`. (`rst_sent` is intentionally NOT
4916    ///    a subset: a queued RST for an already-removed stream is legal.)
4917    /// 3. **RST queue accounting**: the never-decaying `total_rst_streams_queued`
4918    ///    lifetime counter is always `>=` the currently-pending queue length
4919    ///    (CVE-2025-8671 MadeYouReset cap relies on the lifetime counter never
4920    ///    under-counting), and the pending queue stays within its hard cap +1
4921    ///    (the escalation tripwire fires at the cap).
4922    /// 4. **Pending WINDOW_UPDATE bound**: the coalescing map never exceeds the
4923    ///    per-connection cap derived from `max_concurrent_streams`.
4924    /// 5. **Drain/state coupling**: a terminal `GoAway`/`Error` state implies the
4925    ///    connection is draining (`goaway()` sets both); the converse need not
4926    ///    hold (graceful drain stays in a live state).
4927    #[cfg(debug_assertions)]
4928    fn check_invariants<L>(&self, context: &Context<L>)
4929    where
4930        L: ListenerHandler + L7ListenerHandler,
4931    {
4932        // (1) Watermark parity and bound.
4933        debug_assert!(
4934            self.last_stream_id & 1 == 0,
4935            "last_stream_id must stay an even watermark, got {}",
4936            self.last_stream_id
4937        );
4938
4939        // (2) Per-stream caches are subsets of the live stream set, and every
4940        // mapping points at a valid context slot.
4941        debug_assert!(
4942            self.stream_last_activity_at
4943                .keys()
4944                .all(|id| self.streams.contains_key(id)),
4945            "stream_last_activity_at must only track currently-open stream ids"
4946        );
4947        debug_assert!(
4948            self.streams
4949                .values()
4950                .all(|&gid| gid < context.streams.len()),
4951            "every stream mapping must point at a valid context slot"
4952        );
4953
4954        // (3) RST queue accounting.
4955        debug_assert!(
4956            self.total_rst_streams_queued >= self.pending_rst_streams.len(),
4957            "queued-RST lifetime counter ({}) must be >= currently-pending queue ({})",
4958            self.total_rst_streams_queued,
4959            self.pending_rst_streams.len()
4960        );
4961        debug_assert!(
4962            self.pending_rst_streams.len() <= MAX_PENDING_RST_STREAMS + 1,
4963            "pending RST queue must stay within its hard cap (escalates at the cap)"
4964        );
4965
4966        // (4) Pending WINDOW_UPDATE coalescing map bound.
4967        debug_assert!(
4968            self.flow_control.pending_window_updates.len() <= self.max_pending_window_updates,
4969            "pending WINDOW_UPDATE map must stay within its per-connection cap"
4970        );
4971
4972        // (5) Drain/state coupling: terminal states imply draining.
4973        debug_assert!(
4974            !matches!(self.state, H2State::GoAway | H2State::Error) || self.drain.draining,
4975            "GoAway/Error state must imply the connection is draining"
4976        );
4977    }
4978
4979    fn handle_frame<E, L>(
4980        &mut self,
4981        frame: Frame,
4982        wire_payload_len: u32,
4983        context: &mut Context<L>,
4984        endpoint: E,
4985    ) -> MuxResult
4986    where
4987        E: Endpoint,
4988        L: ListenerHandler + L7ListenerHandler,
4989    {
4990        trace!("{} {:#?}", log_context!(self), frame);
4991        // Per-frame-type RX counter. Single chokepoint covers every H2 frame
4992        // type — adding a new `Frame::*` variant fails the build inside the
4993        // helper, keeping the metric breakdown in lock-step with RFC 9113 §6.
4994        count!(h2_frame_rx_metric_key(&frame), 1);
4995        let result = match frame {
4996            Frame::Data(data) => self.handle_data_frame(data, wire_payload_len, context, endpoint),
4997            Frame::Headers(headers) => self.handle_headers_frame(headers, context, endpoint),
4998            Frame::PushPromise(_) => self.handle_push_promise_frame(),
4999            Frame::Priority(priority) => self.handle_priority_frame(priority, context, endpoint),
5000            Frame::RstStream(rst_stream) => {
5001                self.handle_rst_stream_frame(rst_stream, context, endpoint)
5002            }
5003            Frame::Settings(settings) => self.handle_settings_frame(settings, context),
5004            Frame::Ping(ping) => self.handle_ping_frame(ping),
5005            Frame::GoAway(goaway) => self.handle_goaway_frame(goaway, context, endpoint),
5006            Frame::WindowUpdate(wu) => self.handle_window_update_frame(wu, context, endpoint),
5007            Frame::PriorityUpdate(pu) => self.handle_priority_update_frame(pu),
5008            Frame::Continuation(_) => {
5009                // Unreachable: standalone CONTINUATION is rejected in
5010                // `handle_header_state` (RFC 9113 §6.10) and in-block
5011                // CONTINUATION is consumed by the inline header-parsing
5012                // path. Keep a defensive fallback that returns
5013                // PROTOCOL_ERROR rather than panicking in debug builds.
5014                self.attribute_bytes_to_overhead();
5015                warn!(
5016                    "{} CONTINUATION frames are handled inline during header parsing",
5017                    log_context!(self)
5018                );
5019                self.goaway(H2Error::ProtocolError)
5020            }
5021            // RFC 9113 §5.5: unknown frame types MUST be ignored and discarded.
5022            // The parser already consumed the payload; attribute the bytes
5023            // to connection-level overhead and continue.
5024            Frame::Unknown(raw) => {
5025                debug!(
5026                    "{} Ignoring unknown H2 frame type {}",
5027                    log_context!(self),
5028                    raw
5029                );
5030                self.attribute_bytes_to_overhead();
5031                MuxResult::Continue
5032            }
5033        };
5034        // Run-to-completion post-condition: the connection-level cross-field
5035        // invariants must hold after every frame is dispatched, on success and
5036        // on the protocol-error paths alike.
5037        #[cfg(debug_assertions)]
5038        self.check_invariants(context);
5039        result
5040    }
5041
5042    /// RFC 9110 §8.6: Content-Length validation must be skipped for responses
5043    /// where the body is absent by definition:
5044    /// - Responses to HEAD requests (any status)
5045    /// - 1xx informational responses
5046    /// - 204 No Content
5047    /// - 304 Not Modified
5048    fn content_length_exempt(
5049        &self,
5050        context: &crate::protocol::kawa_h1::editor::HttpContext,
5051    ) -> bool {
5052        use crate::protocol::kawa_h1::parser::Method;
5053        // HEAD method responses (only relevant when reading backend responses)
5054        if self.position.is_client() && context.method == Some(Method::Head) {
5055            return true;
5056        }
5057        // 1xx, 204, 304 status codes
5058        if let Some(status) = context.status
5059            && ((100..200).contains(&status) || status == 204 || status == 304)
5060        {
5061            return true;
5062        }
5063        false
5064    }
5065
5066    fn handle_data_frame<E, L>(
5067        &mut self,
5068        data: parser::Data,
5069        wire_payload_len: u32,
5070        context: &mut Context<L>,
5071        mut endpoint: E,
5072    ) -> MuxResult
5073    where
5074        E: Endpoint,
5075        L: ListenerHandler + L7ListenerHandler,
5076    {
5077        // CVE-2019-9518: track empty DATA frames (no payload, no END_STREAM)
5078        if data.payload.is_empty() && !data.end_stream {
5079            let empty_before = self.flood_detector.empty_data_count;
5080            self.flood_detector.empty_data_count += 1;
5081            debug_assert_eq!(
5082                self.flood_detector.empty_data_count,
5083                empty_before + 1,
5084                "empty-DATA flood counter must advance by exactly one per empty frame"
5085            );
5086            check_flood_or_return!(self);
5087        }
5088        let Some(global_stream_id) = self.streams.get(&data.stream_id).copied() else {
5089            // The stream was terminated while data was expected,
5090            // probably due to automatic answer for invalid/unauthorized access.
5091            // RFC 9113 §6.9: we MUST still account for the DATA payload in
5092            // connection-level flow control using the full wire length
5093            // (including pad-length byte and padding), otherwise the window
5094            // shrinks permanently and eventually stalls the connection.
5095            self.flow_control.received_bytes_since_update += wire_payload_len;
5096            let conn_threshold = self.connection_config.initial_connection_window / 2;
5097            if self.flow_control.received_bytes_since_update >= conn_threshold {
5098                let increment = self.flow_control.received_bytes_since_update;
5099                self.queue_window_update(0, increment);
5100                self.flow_control.received_bytes_since_update = 0;
5101                self.readiness.arm_writable();
5102            }
5103            self.attribute_bytes_to_overhead();
5104            return MuxResult::Continue;
5105        };
5106        let mut slice = data.payload;
5107        let stream = &mut context.streams[global_stream_id];
5108        // Unpadded application payload size — what is forwarded to the backend
5109        // and counted against Content-Length.
5110        let content_len = slice.len();
5111        // Full wire-payload size (includes pad-length byte and padding).
5112        // RFC 9113 §5.2: padding counts against flow-control windows.
5113        let wire_len = wire_payload_len as usize;
5114        let cl_exempt = self.content_length_exempt(&stream.context);
5115
5116        // Extract declared content-length and update position-aware data counter
5117        let (data_received, declared_length) = {
5118            let parts = stream.split(&self.position);
5119            *parts.data_received += content_len;
5120            let total = *parts.data_received;
5121            let declared = match parts.rbuffer.body_size {
5122                kawa::BodySize::Length(n) => Some(n),
5123                _ => None,
5124            };
5125            (total, declared)
5126        };
5127
5128        // RFC 9113 §6.9 + §5.2: credit connection-level flow control BEFORE any
5129        // early-return path. Malformed DATA still consumed the peer's send
5130        // window; without crediting it back, repeated bad streams permanently
5131        // shrink the connection window and stall unrelated streams that share
5132        // the same H2 connection. Stream-level credit can stay below — once we
5133        // RST the violating stream, its per-stream window is moot per
5134        // RFC 9113 §6.9 (the receiver discards further frames for the stream).
5135        let conn_threshold = self.connection_config.initial_connection_window / 2;
5136        self.flow_control.received_bytes_since_update += wire_payload_len;
5137        if self.flow_control.received_bytes_since_update >= conn_threshold {
5138            let increment = self.flow_control.received_bytes_since_update;
5139            self.queue_window_update(0, increment);
5140            self.flow_control.received_bytes_since_update = 0;
5141        }
5142
5143        // RFC 9113 §8.1.1: if Content-Length is present, total DATA payload
5144        // must not exceed the declared length (check on every frame).
5145        // RFC 9110 §8.6: skip for HEAD/1xx/204/304 responses (body absent by definition).
5146        if !cl_exempt
5147            && let Some(expected) = declared_length
5148            && data_received > expected
5149        {
5150            error!(
5151                "{} Content-Length mismatch: received {} > declared {}",
5152                log_context!(self),
5153                data_received,
5154                expected
5155            );
5156            // Pair WRITABLE arming with the queued connection-level
5157            // WINDOW_UPDATE before returning; otherwise the credit sits
5158            // until the next inbound frame on this connection.
5159            if !self.flow_control.pending_window_updates.is_empty() {
5160                self.readiness.arm_writable();
5161            }
5162            let result = self.reset_stream(
5163                data.stream_id,
5164                global_stream_id,
5165                context,
5166                endpoint,
5167                H2Error::ProtocolError,
5168            );
5169            self.remove_dead_stream(data.stream_id, global_stream_id);
5170            return result;
5171        }
5172
5173        let stream = &mut context.streams[global_stream_id];
5174        self.attribute_bytes_to_stream(&mut stream.metrics);
5175        let stream_state = stream.state;
5176        let is_unlinked = matches!(stream_state, StreamState::Unlinked);
5177        let parts = stream.split(&self.position);
5178        let kawa = parts.rbuffer;
5179        self.position.count_bytes_in(parts.metrics, content_len);
5180
5181        // Stream-level flow control (only if stream is still open).
5182        // Connection-level credit was already applied above the CL check so
5183        // malformed DATA cannot starve the connection window for other streams.
5184        if !data.end_stream {
5185            self.queue_window_update(data.stream_id, wire_payload_len);
5186        }
5187
5188        // If we have pending updates, ensure we get a writable event.
5189        // Must use signal_pending_write() — not just interest.insert() — because
5190        // under edge-triggered epoll the WRITABLE event bit may have been consumed
5191        // by a previous write cycle. Without the event bit set, filter_interest()
5192        // returns 0 and the WINDOW_UPDATEs never get flushed, stalling the client.
5193        if !self.flow_control.pending_window_updates.is_empty() {
5194            self.readiness.arm_writable();
5195        }
5196
5197        // Refresh per-stream idle timer on non-empty DATA.
5198        // Empty DATA frames (CVE-2019-9518 vector) must NOT reset the timer,
5199        // otherwise an attacker can keep a stream alive indefinitely with
5200        // zero-length frames while pinning a MAX_CONCURRENT_STREAMS slot.
5201        if content_len > 0
5202            && let Some(t) = self.stream_last_activity_at.get_mut(&data.stream_id)
5203        {
5204            *t = Instant::now();
5205        }
5206
5207        if is_unlinked {
5208            // Backend is gone but client is still sending DATA.
5209            // Discard the data (flow control updates were already
5210            // queued above) to prevent the buffer from filling up.
5211            kawa.storage.clear();
5212            if data.end_stream {
5213                kawa.parsing_phase = kawa::ParsingPhase::Terminated;
5214                self.mark_end_of_stream(stream);
5215            }
5216        } else {
5217            // Advance storage.head by the full wire payload length so the
5218            // next frame doesn't read stale pad-length+padding bytes.
5219            slice.start = slice.start.saturating_add(kawa.storage.head as u32);
5220            kawa.storage.head += wire_len;
5221
5222            // Emit chunk framing for chunked transfer encoding (H2→H1 path).
5223            // H2 converter ignores ChunkHeader and end_chunk Flags, so this is safe for H2→H2.
5224            if kawa.body_size == kawa::BodySize::Chunked && content_len > 0 {
5225                let hex_len = {
5226                    let mut buf = Vec::with_capacity(16);
5227                    let _ = write!(buf, "{content_len:x}");
5228                    buf
5229                };
5230                kawa.push_block(kawa::Block::ChunkHeader(kawa::ChunkHeader {
5231                    length: kawa::Store::from_vec(hex_len),
5232                }));
5233            }
5234
5235            kawa.push_block(kawa::Block::Chunk(kawa::Chunk {
5236                data: kawa::Store::Slice(slice),
5237            }));
5238
5239            if kawa.body_size == kawa::BodySize::Chunked && content_len > 0 {
5240                kawa.push_block(kawa::Block::Flags(kawa::Flags {
5241                    end_body: false,
5242                    end_chunk: true,
5243                    end_header: false,
5244                    end_stream: false,
5245                }));
5246            }
5247
5248            if data.end_stream {
5249                // RFC 9113 §8.1.1: on end_stream, total DATA must equal Content-Length.
5250                // RFC 9110 §8.6: skip for HEAD/1xx/204/304 responses.
5251                if !cl_exempt
5252                    && let Some(expected) = declared_length
5253                    && data_received != expected
5254                {
5255                    error!(
5256                        "{} Content-Length mismatch: received {} != declared {}",
5257                        log_context!(self),
5258                        data_received,
5259                        expected
5260                    );
5261                    let result = self.reset_stream(
5262                        data.stream_id,
5263                        global_stream_id,
5264                        context,
5265                        endpoint,
5266                        H2Error::ProtocolError,
5267                    );
5268                    self.remove_dead_stream(data.stream_id, global_stream_id);
5269                    return result;
5270                }
5271                let is_chunked = kawa.body_size == kawa::BodySize::Chunked;
5272                kawa.push_block(kawa::Block::Flags(kawa::Flags {
5273                    end_body: true,
5274                    end_chunk: is_chunked,
5275                    end_header: false,
5276                    end_stream: true,
5277                }));
5278                kawa.parsing_phase = kawa::ParsingPhase::Terminated;
5279                self.mark_end_of_stream(stream);
5280            }
5281            if let StreamState::Linked(token) = stream_state {
5282                // Mirror of h1.rs:361-368 for the H2-backend → H2-frontend
5283                // path: edge-triggered epoll will NOT re-fire for bytes we
5284                // just pushed into stream.back; the synthetic event is the
5285                // only wake path. LIFECYCLE invariant 15.
5286                endpoint.readiness_mut(token).arm_writable();
5287                incr!(names::h2::SIGNAL_WRITABLE_REARMED_PEER_DATA);
5288            }
5289        }
5290        MuxResult::Continue
5291    }
5292
5293    fn handle_headers_frame<E, L>(
5294        &mut self,
5295        headers: Headers,
5296        context: &mut Context<L>,
5297        mut endpoint: E,
5298    ) -> MuxResult
5299    where
5300        E: Endpoint,
5301        L: ListenerHandler + L7ListenerHandler,
5302    {
5303        // HEADERS frames represent real application activity (new request
5304        // or response). Reset the timeout since the peer is actively
5305        // communicating, unlike control frames (PING, WINDOW_UPDATE).
5306        self.timeout_container.reset();
5307        if !headers.end_headers {
5308            // CVE-2024-27316: only initialize tracking on the very first HEADERS
5309            // fragment, not on re-entries from ContinuationFrame (which call
5310            // handle_frame(Frame::Headers) with the accumulated header block).
5311            if self.flood_detector.continuation_count == 0 {
5312                self.flood_detector.accumulated_header_size = headers.header_block_fragment.len;
5313            }
5314            debug!(
5315                "{} FRAGMENT: stream_id={}, len={}",
5316                log_context!(self),
5317                headers.stream_id,
5318                self.zero.storage.data().len()
5319            );
5320            self.state = H2State::ContinuationHeader(headers);
5321            return MuxResult::Continue;
5322        }
5323        // Header block is complete — reset CONTINUATION counters
5324        self.flood_detector.reset_continuation();
5325        // can this fail?
5326        let stream_id = headers.stream_id;
5327        let Some(global_stream_id) = self.streams.get(&stream_id).copied() else {
5328            error!(
5329                "{} Handling Headers frame with no attached stream {:#?}",
5330                log_context!(self),
5331                self
5332            );
5333            incr!(names::h2::HEADERS_NO_STREAM_ERROR);
5334            self.attribute_bytes_to_overhead();
5335            return self.force_disconnect();
5336        };
5337
5338        // Refresh per-stream idle timer on HEADERS (response headers or trailers
5339        // on an existing stream). Initial HEADERS that create the stream already
5340        // set the timestamp in create_stream().
5341        if let Some(t) = self.stream_last_activity_at.get_mut(&stream_id) {
5342            *t = Instant::now();
5343        }
5344
5345        if let Some(priority) = &headers.priority
5346            && self.prioriser.push_priority(stream_id, priority.clone())
5347        {
5348            self.reset_stream(
5349                stream_id,
5350                global_stream_id,
5351                context,
5352                endpoint,
5353                H2Error::ProtocolError,
5354            );
5355            self.remove_dead_stream(stream_id, global_stream_id);
5356            return MuxResult::Continue;
5357        }
5358
5359        let stream = &mut context.streams[global_stream_id];
5360        self.attribute_bytes_to_stream(&mut stream.metrics);
5361        let kawa = &mut self.zero;
5362        let buffer = headers.header_block_fragment.data(kawa.storage.buffer());
5363        let stream = &mut context.streams[global_stream_id];
5364        let parts = &mut stream.split(&self.position);
5365        let was_initial = parts.rbuffer.is_initial();
5366        let elide_x_real_ip = parts.context.elide_x_real_ip;
5367        let status = pkawa::handle_header(
5368            &mut self.decoder,
5369            &mut self.prioriser,
5370            stream_id,
5371            parts.rbuffer,
5372            buffer,
5373            headers.end_stream,
5374            parts.context,
5375            self.flood_detector.config.max_header_list_size,
5376            self.flood_detector.config.max_header_fields,
5377            elide_x_real_ip,
5378        );
5379        kawa.storage.clear();
5380        if let Err((error, global)) = status {
5381            match self.position {
5382                Position::Client(..) => incr!(names::http::BACKEND_PARSE_ERRORS),
5383                Position::Server => incr!(names::http::FRONTEND_PARSE_ERRORS),
5384            }
5385            if global {
5386                error!(
5387                    "{} GOT GLOBAL ERROR WHILE PROCESSING HEADERS",
5388                    log_context!(self)
5389                );
5390                return self.goaway(error);
5391            } else {
5392                let result =
5393                    self.reset_stream(stream_id, global_stream_id, context, endpoint, error);
5394                self.remove_dead_stream(stream_id, global_stream_id);
5395                return result;
5396            }
5397        }
5398        if headers.end_stream {
5399            // RFC 9113 §8.1.1: when END_STREAM arrives via trailers,
5400            // validate that total DATA received matches Content-Length.
5401            // RFC 9110 §8.6: skip for HEAD/1xx/204/304 responses.
5402            if !was_initial && !self.content_length_exempt(&stream.context) {
5403                let parts = stream.split(&self.position);
5404                if let kawa::BodySize::Length(expected) = parts.rbuffer.body_size
5405                    && *parts.data_received != expected
5406                {
5407                    error!(
5408                        "{} Content-Length mismatch on trailers: received {} != declared {}",
5409                        log_context!(self),
5410                        *parts.data_received,
5411                        expected
5412                    );
5413                    let result = self.reset_stream(
5414                        stream_id,
5415                        global_stream_id,
5416                        context,
5417                        endpoint,
5418                        H2Error::ProtocolError,
5419                    );
5420                    self.remove_dead_stream(stream_id, global_stream_id);
5421                    return result;
5422                }
5423            }
5424            self.mark_end_of_stream(stream);
5425        }
5426        if let StreamState::Linked(token) = stream.state {
5427            // Mirror of handle_data_frame's rearm. LIFECYCLE invariant 15.
5428            endpoint.readiness_mut(token).arm_writable();
5429            incr!(names::h2::SIGNAL_WRITABLE_REARMED_PEER_HEADERS);
5430        }
5431        // was_initial prevents trailers from triggering connection
5432        if was_initial && self.position.is_server() {
5433            incr!(names::http::REQUESTS);
5434            gauge_add!(names::http::ACTIVE_REQUESTS, 1);
5435            stream.metrics.service_start();
5436            stream.request_counted = true;
5437            stream.state = StreamState::Link;
5438            context.pending_links.push_back(global_stream_id);
5439        }
5440        MuxResult::Continue
5441    }
5442
5443    fn handle_push_promise_frame(&mut self) -> MuxResult {
5444        self.attribute_bytes_to_overhead();
5445        match self.position {
5446            Position::Client(..) => {
5447                // RFC 9113 §8.4: Server push is deprecated. Sozu never sends
5448                // SETTINGS_ENABLE_PUSH=1, so receiving PUSH_PROMISE is a protocol error.
5449                error!(
5450                    "{} Received PUSH_PROMISE but server push is not supported",
5451                    log_context!(self)
5452                );
5453                self.goaway(H2Error::ProtocolError)
5454            }
5455            Position::Server => {
5456                // Clients must never send PUSH_PROMISE (RFC 9113 §8.4)
5457                error!("{} Received PUSH_PROMISE from client", log_context!(self));
5458                self.goaway(H2Error::ProtocolError)
5459            }
5460        }
5461    }
5462
5463    fn handle_priority_frame<E, L>(
5464        &mut self,
5465        priority: parser::Priority,
5466        context: &mut Context<L>,
5467        endpoint: E,
5468    ) -> MuxResult
5469    where
5470        E: Endpoint,
5471        L: ListenerHandler + L7ListenerHandler,
5472    {
5473        if let Some(global_stream_id) = self.streams.get(&priority.stream_id).copied() {
5474            let stream = &mut context.streams[global_stream_id];
5475            self.attribute_bytes_to_stream(&mut stream.metrics);
5476        } else {
5477            self.attribute_bytes_to_overhead();
5478        }
5479        // Pass 3 Medium #4: standalone PRIORITY frames can arrive for any
5480        // peer-chosen stream ID. Accept only currently-open streams and a
5481        // small idle look-ahead window; everything else is dropped before
5482        // it can feed memory into the priority map.
5483        if self.prioriser.push_priority_guarded(
5484            priority.stream_id,
5485            priority.inner,
5486            self.last_stream_id,
5487            &self.streams,
5488        ) {
5489            if let Some(global_stream_id) = self.streams.get(&priority.stream_id).copied() {
5490                let result = self.reset_stream(
5491                    priority.stream_id,
5492                    global_stream_id,
5493                    context,
5494                    endpoint,
5495                    H2Error::ProtocolError,
5496                );
5497                self.remove_dead_stream(priority.stream_id, global_stream_id);
5498                return result;
5499            } else {
5500                error!(
5501                    "{} INVALID PRIORITY RECEIVED ON INVALID STREAM",
5502                    log_context!(self)
5503                );
5504                return self.goaway(H2Error::ProtocolError);
5505            }
5506        }
5507        MuxResult::Continue
5508    }
5509
5510    /// RFC 9218 §7.1: PRIORITY_UPDATE reprioritizes an open or idle-soon
5511    /// stream at the connection level. Decodes the priority field value
5512    /// (same grammar as the `priority` request header, `parse_rfc9218_priority`)
5513    /// and pushes it into the `Prioriser` through the same guarded path used
5514    /// for standalone PRIORITY frames — the guard bounds memory against a
5515    /// client spamming PRIORITY_UPDATE for far-future stream IDs.
5516    ///
5517    /// Prioritized stream ID `0` is a connection-level `PROTOCOL_ERROR`
5518    /// (RFC 9218 §7.1). For any other ID that is not currently open or
5519    /// within the idle look-ahead budget, the update is silently dropped
5520    /// (matches the PRIORITY-frame guard semantics — no state change).
5521    fn handle_priority_update_frame(&mut self, pu: parser::PriorityUpdate) -> MuxResult {
5522        self.attribute_bytes_to_overhead();
5523        if pu.prioritized_stream_id == 0 {
5524            error!(
5525                "{} PRIORITY_UPDATE with prioritized_stream_id=0 (RFC 9218 §7.1)",
5526                log_context!(self)
5527            );
5528            return self.goaway(H2Error::ProtocolError);
5529        }
5530        let (urgency, incremental) = pkawa::parse_rfc9218_priority(&pu.priority_field_value);
5531        let (prev_urgency, _) = self.prioriser.get(&pu.prioritized_stream_id);
5532        trace!(
5533            "{} PRIORITY_UPDATE stream={} urgency={}->{} incremental={} rearmed_writable=true",
5534            log_context!(self),
5535            pu.prioritized_stream_id,
5536            prev_urgency,
5537            urgency,
5538            incremental
5539        );
5540        let _ = self.prioriser.push_priority_guarded(
5541            pu.prioritized_stream_id,
5542            parser::PriorityPart::Rfc9218 {
5543                urgency,
5544                incremental,
5545            },
5546            self.last_stream_id,
5547            &self.streams,
5548        );
5549        // LIFECYCLE invariant 15: reprioritisation only changes ordering for
5550        // the NEXT write pass. Under ET epoll, if finalize_write already
5551        // stripped WRITABLE, the scheduler won't re-run without a synthetic
5552        // wake — pair the interest insert with signal_pending_write.
5553        self.readiness.arm_writable();
5554        incr!(names::h2::SIGNAL_WRITABLE_REARMED_PRIORITY_UPDATE);
5555        MuxResult::Continue
5556    }
5557
5558    fn handle_rst_stream_frame<E, L>(
5559        &mut self,
5560        rst_stream: parser::RstStream,
5561        context: &mut Context<L>,
5562        mut endpoint: E,
5563    ) -> MuxResult
5564    where
5565        E: Endpoint,
5566        L: ListenerHandler + L7ListenerHandler,
5567    {
5568        // Per-error-code counter for the inbound RST. Emitted before the
5569        // flood-detector trip check so even a connection that gets terminated
5570        // by `handle_flood_violation` shows up in the per-code breakdown
5571        // (the dedicated `h2.flood.violation.rst_stream_*` series tracks the
5572        // mitigation event itself).
5573        count!(metric_for_rst_stream_received(rst_stream.error_code), 1);
5574        // CVE-2023-44487 Rapid Reset + CVE-2019-9514: track RST_STREAM rate.
5575        let rst_count_before = self.flood_detector.rst_stream_count;
5576        self.flood_detector.rst_stream_count += 1;
5577        debug_assert_eq!(
5578            self.flood_detector.rst_stream_count,
5579            rst_count_before + 1,
5580            "per-window RST_STREAM counter must advance by exactly one per inbound RST"
5581        );
5582        check_flood_or_return!(self);
5583        // Additional CVE-2023-44487 mitigation: lifetime cap on RST_STREAM
5584        // frames received. The per-window counter above half-decays, so a
5585        // patient client can keep ~50 RST/s forever; a never-decaying
5586        // lifetime counter puts an absolute ceiling on that amplification.
5587        // Streams whose backend response has not yet started count toward a
5588        // much lower "abusive" ceiling — this is the signature Rapid Reset
5589        // pattern where the attacker pays one RST frame and we pay a
5590        // backend round-trip for each.
5591        //
5592        // "Response started" here means the Server has begun producing
5593        // response bytes (backend kawa buffer past its initial phase). For
5594        // the Client position the concept does not apply symmetrically
5595        // (RSTs received from the backend are rare and benign), so we
5596        // conservatively flag them as abusive too — lifetime cap still
5597        // dominates in practice.
5598        let response_started = match self.streams.get(&rst_stream.stream_id) {
5599            Some(global_stream_id) => {
5600                let stream = &context.streams[*global_stream_id];
5601                !stream.back.is_initial()
5602            }
5603            // Stream already gone (e.g. closed, not yet registered) —
5604            // treat as response-started to avoid over-counting benign
5605            // races as abusive.
5606            None => true,
5607        };
5608        if let Some(violation) = self.flood_detector.record_rst_lifetime(response_started) {
5609            return self.handle_flood_violation(violation);
5610        }
5611        // Rapid Reset signature (CVE-2023-44487): a RST that arrives before the
5612        // backend has begun answering. Emitted alongside the per-code counter
5613        // so the SOC can alert on the rate of pre-response RSTs without
5614        // having to differentiate by error code.
5615        if !response_started {
5616            count!(names::h2::RST_STREAM_RECEIVED_PRE_RESPONSE_START, 1);
5617        }
5618        debug!(
5619            "{} RstStream({} -> {})",
5620            log_context!(self),
5621            rst_stream.error_code,
5622            H2Error::try_from(rst_stream.error_code).map_or("UNKNOWN_ERROR", |e| e.as_str())
5623        );
5624        // Compute totals before removing the stream from the map,
5625        // so the removed stream's bytes are included in the total.
5626        let rst_byte_totals = self.compute_stream_byte_totals(context);
5627        if let Some(global_stream_id) = self.streams.get(&rst_stream.stream_id).copied() {
5628            let stream = &mut context.streams[global_stream_id];
5629            self.attribute_bytes_to_stream(&mut stream.metrics);
5630            let linked_token = stream.linked_token();
5631            let (client_rtt, server_rtt) =
5632                Self::snapshot_rtts(&self.position, &self.socket, &endpoint, linked_token);
5633            if let Some(token) = linked_token {
5634                endpoint.end_stream(token, global_stream_id, context);
5635            }
5636            let stream = &mut context.streams[global_stream_id];
5637            match &self.position {
5638                // Inbound RST_STREAM on the backend side terminates the in-flight
5639                // request without going through Connection::end_stream (the normal
5640                // place where Backend.active_requests is decremented), so do the
5641                // bookkeeping explicitly here to avoid leaking load counters.
5642                Position::Client(_, backend, BackendStatus::Connected) => {
5643                    let mut backend_borrow = backend.borrow_mut();
5644                    backend_borrow.active_requests =
5645                        backend_borrow.active_requests.saturating_sub(1);
5646                }
5647                Position::Client(..) => {}
5648                Position::Server => {
5649                    self.distribute_overhead(&mut stream.metrics, rst_byte_totals);
5650                    // This is a special case, normally, all stream are terminated by the server
5651                    // when the last byte of the response is written. Here, the reset is requested
5652                    // on the server endpoint and immediately terminates, shortcutting the other path
5653                    stream.metrics.backend_stop();
5654                    stream.generate_access_log(
5655                        true,
5656                        Some("H2::ResetFrame"),
5657                        context.listener.clone(),
5658                        client_rtt,
5659                        server_rtt,
5660                    );
5661                    stream.state = StreamState::Recycle;
5662                }
5663            }
5664            // Retire from streams/prioriser/stream_last_activity_at and
5665            // invalidate expect_write/expect_read if they reference this gid.
5666            self.remove_dead_stream(rst_stream.stream_id, global_stream_id);
5667        } else {
5668            self.attribute_bytes_to_overhead();
5669        }
5670        MuxResult::Continue
5671    }
5672
5673    fn handle_settings_frame<L>(
5674        &mut self,
5675        settings: parser::Settings,
5676        context: &mut Context<L>,
5677    ) -> MuxResult
5678    where
5679        L: ListenerHandler + L7ListenerHandler,
5680    {
5681        if settings.ack {
5682            // RFC 9113 §6.5: SETTINGS ACK must have empty payload
5683            if !settings.settings.is_empty() {
5684                error!("{} SETTINGS ACK with non-empty payload", log_context!(self));
5685                return self.goaway(H2Error::FrameSizeError);
5686            }
5687            // RFC 9113 §6.5: peer acknowledged our SETTINGS — clear timeout
5688            self.settings_sent_at = None;
5689            // RFC 7541 §4.2: sync the decoder's max allowed table size with
5690            // what we advertised. Currently a no-op (settings don't change at
5691            // runtime), but guards against future runtime SETTINGS updates.
5692            self.decoder.set_max_allowed_table_size(
5693                self.local_settings.settings_header_table_size as usize,
5694            );
5695            self.attribute_bytes_to_overhead();
5696            return MuxResult::Continue;
5697        }
5698        // CVE-2019-9515: track SETTINGS frame rate
5699        let settings_count_before = self.flood_detector.settings_count;
5700        let settings_lifetime_before = self.flood_detector.total_settings_received_lifetime;
5701        self.flood_detector.settings_count += 1;
5702        self.flood_detector.total_settings_received_lifetime = self
5703            .flood_detector
5704            .total_settings_received_lifetime
5705            .saturating_add(1);
5706        debug_assert_eq!(
5707            self.flood_detector.settings_count,
5708            settings_count_before + 1,
5709            "per-window SETTINGS counter must advance by one per non-ACK SETTINGS"
5710        );
5711        debug_assert!(
5712            self.flood_detector.total_settings_received_lifetime > settings_lifetime_before
5713                || settings_lifetime_before == u32::MAX,
5714            "lifetime SETTINGS counter must advance (or already be saturated)"
5715        );
5716        check_flood_or_return!(self);
5717        for setting in settings.settings {
5718            let v = setting.value;
5719            let mut is_error = false;
5720            #[rustfmt::skip]
5721            match setting.identifier {
5722                parser::SETTINGS_HEADER_TABLE_SIZE => {
5723                    // Cap to the configured maximum — a malicious peer can
5724                    // advertise up to 4 GB to inflate HPACK encoder memory.
5725                    let cap = self.flood_detector.config.max_header_table_size;
5726                    let capped = v.min(cap);
5727                    self.peer_settings.settings_header_table_size = capped;
5728                    self.encoder.set_max_table_size(capped as usize);
5729                    // RFC 7541 §4.2 / §6.3: queue a dynamic-table-size-update
5730                    // HPACK directive for the next header block we emit.
5731                    // Without it, the peer's decoder keeps its previous (possibly
5732                    // larger) table cap and our encoder-side change is silent
5733                    // — conformance suites (h2spec `hpack/4.2`) will flag it.
5734                    self.pending_table_size_update = Some(capped);
5735                },
5736                parser::SETTINGS_ENABLE_PUSH       => { self.peer_settings.settings_enable_push = v == 1;             is_error |= v > 1 },
5737                parser::SETTINGS_MAX_CONCURRENT_STREAMS => { self.peer_settings.settings_max_concurrent_streams = v },
5738                parser::SETTINGS_INITIAL_WINDOW_SIZE    => { is_error |= self.update_initial_window_size(v, context) },
5739                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) },
5740                parser::SETTINGS_MAX_HEADER_LIST_SIZE   => { self.peer_settings.settings_max_header_list_size = v },
5741                parser::SETTINGS_ENABLE_CONNECT_PROTOCOL => { self.peer_settings.settings_enable_connect_protocol = v == 1; is_error |= v > 1 },
5742                parser::SETTINGS_NO_RFC7540_PRIORITIES   => { self.peer_settings.settings_no_rfc7540_priorities = v == 1;   is_error |= v > 1 },
5743                other => { warn!("Unknown setting_id: {}, we MUST ignore this", other); self.flood_detector.glitch_count += 1 },
5744            };
5745            if is_error {
5746                error!("{} INVALID SETTING", log_context!(self));
5747                return self.goaway(H2Error::ProtocolError);
5748            }
5749        }
5750
5751        self.attribute_bytes_to_overhead();
5752
5753        // Enlarge the connection-level receive window for backend H2
5754        // connections (Position::Client). The server side does this in
5755        // the ServerSettings writable path, but the client needs to do
5756        // it here after receiving the server's initial SETTINGS.
5757        if self.position.is_client()
5758            && self.flow_control.window <= DEFAULT_INITIAL_WINDOW_SIZE as i32
5759        {
5760            let increment = self
5761                .connection_config
5762                .initial_connection_window
5763                .saturating_sub(DEFAULT_INITIAL_WINDOW_SIZE);
5764            if increment > 0 {
5765                self.queue_window_update(0, increment);
5766            }
5767            // Do NOT increment flow_control.window here: sending our own
5768            // WINDOW_UPDATE enlarges the peer's send allowance, not ours.
5769            // Our send window is only updated by WINDOW_UPDATEs we receive
5770            // from the peer (RFC 9113 §6.9).
5771        }
5772
5773        let kawa = &mut self.zero;
5774        let ack = &serializer::SETTINGS_ACKNOWLEDGEMENT;
5775        let buf = kawa.storage.space();
5776        if buf.len() < ack.len() {
5777            error!(
5778                "{} No space in zero buffer for SETTINGS ACK ({} available, {} needed)",
5779                log_context!(self),
5780                buf.len(),
5781                ack.len()
5782            );
5783            return self.force_disconnect();
5784        }
5785        buf[..ack.len()].copy_from_slice(ack);
5786        kawa.storage.fill(ack.len());
5787
5788        self.readiness.interest.insert(Ready::WRITABLE);
5789        self.readiness.interest.remove(Ready::READABLE);
5790        self.expect_write = Some(H2StreamId::Zero);
5791        self.readiness.signal_pending_write();
5792        MuxResult::Continue
5793    }
5794
5795    fn handle_ping_frame(&mut self, ping: parser::Ping) -> MuxResult {
5796        if ping.ack {
5797            self.attribute_bytes_to_overhead();
5798            return MuxResult::Continue;
5799        }
5800        // CVE-2019-9512: track non-ACK PING frame rate
5801        let ping_count_before = self.flood_detector.ping_count;
5802        let ping_lifetime_before = self.flood_detector.total_ping_received_lifetime;
5803        self.flood_detector.ping_count += 1;
5804        self.flood_detector.total_ping_received_lifetime = self
5805            .flood_detector
5806            .total_ping_received_lifetime
5807            .saturating_add(1);
5808        debug_assert_eq!(
5809            self.flood_detector.ping_count,
5810            ping_count_before + 1,
5811            "per-window PING counter must advance by one per non-ACK PING"
5812        );
5813        debug_assert!(
5814            self.flood_detector.total_ping_received_lifetime > ping_lifetime_before
5815                || ping_lifetime_before == u32::MAX,
5816            "lifetime PING counter must advance (or already be saturated)"
5817        );
5818        check_flood_or_return!(self);
5819        self.attribute_bytes_to_overhead();
5820        let kawa = &mut self.zero;
5821        let ping_response_size = serializer::PING_ACKNOWLEDGEMENT_HEADER.len() + 8;
5822        if kawa.storage.space().len() < ping_response_size {
5823            error!(
5824                "{} No space in zero buffer for PING response ({} available, {} needed)",
5825                log_context!(self),
5826                kawa.storage.space().len(),
5827                ping_response_size
5828            );
5829            return self.force_disconnect();
5830        }
5831        match serializer::gen_ping_acknowledgement(kawa.storage.space(), &ping.payload) {
5832            Ok((_, size)) => {
5833                kawa.storage.fill(size);
5834                incr!(names::h2::FRAMES_TX_PING_ACK);
5835            }
5836            Err(error) => {
5837                error!(
5838                    "{} Could not serialize PingFrame: {:?}",
5839                    log_context!(self),
5840                    error
5841                );
5842                return self.force_disconnect();
5843            }
5844        };
5845        self.readiness.interest.insert(Ready::WRITABLE);
5846        self.readiness.interest.remove(Ready::READABLE);
5847        self.expect_write = Some(H2StreamId::Zero);
5848        self.readiness.signal_pending_write();
5849        MuxResult::Continue
5850    }
5851
5852    fn handle_goaway_frame<E, L>(
5853        &mut self,
5854        goaway: parser::GoAway,
5855        context: &mut Context<L>,
5856        mut endpoint: E,
5857    ) -> MuxResult
5858    where
5859        E: Endpoint,
5860        L: ListenerHandler + L7ListenerHandler,
5861    {
5862        self.attribute_bytes_to_overhead();
5863        let error_name =
5864            H2Error::try_from(goaway.error_code).map_or("UNKNOWN_ERROR", |e| e.as_str());
5865        if goaway.error_code == H2Error::NoError as u32 {
5866            debug!(
5867                "{} Received GOAWAY: last_stream_id={}, error={}, debug_data={:?}",
5868                log_context!(self),
5869                goaway.last_stream_id,
5870                error_name,
5871                goaway.additional_debug_data
5872            );
5873        } else {
5874            // Peer-originated failure: no variant of H2Error from a peer
5875            // implies a sozu bug. Impact handling is separate (retry above
5876            // `last_stream_id`, RST_STREAM for consumed streams) and logs
5877            // its own details below, so the summary drops to `warn!`.
5878            warn!(
5879                "{} Received GOAWAY: last_stream_id={}, error={}, debug_data={:?}",
5880                log_context!(self),
5881                goaway.last_stream_id,
5882                error_name,
5883                goaway.additional_debug_data
5884            );
5885        }
5886        count!(metric_for_goaway_received(goaway.error_code), 1);
5887        // RFC 9113 §6.8: begin graceful drain.
5888        self.drain.draining = true;
5889        self.drain.peer_last_stream_id = Some(goaway.last_stream_id);
5890
5891        // Streams with ID > last_stream_id were NOT processed by the peer.
5892        // Mark them for retry (StreamState::Link) so they can be retried
5893        // on a new connection.
5894        // IMPORTANT: do NOT call endpoint.end_stream() here — that would
5895        // remove the stream from the frontend's H2 stream map and send
5896        // RST_STREAM to the client, killing the request instead of retrying it.
5897        let mut retry_streams = Vec::new();
5898        for (&stream_id, &global_stream_id) in &self.streams {
5899            if stream_id > goaway.last_stream_id {
5900                retry_streams.push((stream_id, global_stream_id));
5901            }
5902        }
5903        for (stream_id, global_stream_id) in &retry_streams {
5904            // Remove from reverse index before transitioning away from Linked.
5905            if let StreamState::Linked(token) = context.streams[*global_stream_id].state {
5906                remove_backend_stream(&mut context.backend_streams, token, *global_stream_id);
5907            }
5908            let stream = &mut context.streams[*global_stream_id];
5909            if stream.front.consumed {
5910                // Request was already sent to this backend — we can't
5911                // replay it. Use the linked token's readiness (via endpoint)
5912                // so the RST_STREAM reaches the client.
5913                debug!(
5914                    "{} GOAWAY: stream {} already consumed, cannot retry",
5915                    log_context!(self),
5916                    stream_id
5917                );
5918                if let StreamState::Linked(token) = stream.state {
5919                    let front_readiness = endpoint.readiness_mut(token);
5920                    forcefully_terminate_answer(stream, front_readiness, H2Error::RefusedStream);
5921                } else {
5922                    warn!(
5923                        "{} GOAWAY: stream {} consumed but not Linked, cannot notify frontend",
5924                        log_context!(self),
5925                        stream_id
5926                    );
5927                }
5928            } else {
5929                stream.state = StreamState::Link;
5930                context.pending_links.push_back(*global_stream_id);
5931            }
5932            // Both retry (!consumed) and terminated (consumed) paths remove the
5933            // stream from self.streams without going through Connection::end_stream,
5934            // so decrement Backend.active_requests here to keep load metrics honest.
5935            if let Position::Client(_, backend, BackendStatus::Connected) = &self.position {
5936                let mut backend_borrow = backend.borrow_mut();
5937                backend_borrow.active_requests = backend_borrow.active_requests.saturating_sub(1);
5938            }
5939            // Retire from streams/prioriser/stream_last_activity_at and
5940            // invalidate expect_write/expect_read if they reference this gid.
5941            self.remove_dead_stream(*stream_id, *global_stream_id);
5942        }
5943
5944        // If no active streams remain, close immediately
5945        if self.streams.is_empty() {
5946            return self.goaway(H2Error::NoError);
5947        }
5948
5949        // Otherwise, let remaining streams (ID <= last_stream_id) complete.
5950        // The connection will be closed when all streams finish.
5951        MuxResult::Continue
5952    }
5953
5954    fn handle_window_update_frame<E, L>(
5955        &mut self,
5956        wu: WindowUpdate,
5957        context: &mut Context<L>,
5958        endpoint: E,
5959    ) -> MuxResult
5960    where
5961        E: Endpoint,
5962        L: ListenerHandler + L7ListenerHandler,
5963    {
5964        let stream_id = wu.stream_id;
5965        let increment = wu.increment;
5966
5967        // RFC 9113 §6.9: increment of 0 MUST be treated as an error.
5968        // Connection-level (stream 0) -> connection error (GOAWAY).
5969        // Stream-level -> stream error (RST_STREAM).
5970        if increment == 0 {
5971            if stream_id == 0 {
5972                error!(
5973                    "{} WINDOW_UPDATE with zero increment on connection (stream 0)",
5974                    log_context!(self)
5975                );
5976                return self.goaway(H2Error::ProtocolError);
5977            } else {
5978                error!(
5979                    "{} WINDOW_UPDATE with zero increment on stream {}",
5980                    log_context!(self),
5981                    stream_id
5982                );
5983                if let Some(global_stream_id) = self.streams.get(&stream_id).copied() {
5984                    let result = self.reset_stream(
5985                        stream_id,
5986                        global_stream_id,
5987                        context,
5988                        endpoint,
5989                        H2Error::ProtocolError,
5990                    );
5991                    self.remove_dead_stream(stream_id, global_stream_id);
5992                    return result;
5993                }
5994                // Stream not in map (already closed) — treat as glitch
5995                self.flood_detector.glitch_count += 1;
5996                check_flood_or_return!(self);
5997                self.attribute_bytes_to_overhead();
5998                return MuxResult::Continue;
5999            }
6000        }
6001
6002        // The parser masks the reserved bit (STREAM_ID_MASK), so increment <=
6003        // 2^31-1 and try_from always succeeds. Use try_from rather than `as` to
6004        // guard against a future parser change that drops the mask.
6005        let increment = i32::try_from(increment).unwrap_or(i32::MAX);
6006        // RFC 9113 §6.9: a non-zero WINDOW_UPDATE increment is in [1, 2^31-1].
6007        // Zero was short-circuited above; this asserts the masked value is a
6008        // legal positive increment before we add it to a window.
6009        debug_assert!(
6010            increment > 0,
6011            "WINDOW_UPDATE increment must be strictly positive at this point (zero handled above)"
6012        );
6013        if stream_id == 0 {
6014            // Count connection-level WINDOW_UPDATEs before touching the window
6015            // so a per-window flood stops us before we pay the arithmetic cost
6016            // on a million-frame burst. Zero-increment frames short-circuited
6017            // above, so every increment here is a legal-looking rate consumer.
6018            let wu0_before = self.flood_detector.window_update_stream0_count;
6019            self.flood_detector.window_update_stream0_count = self
6020                .flood_detector
6021                .window_update_stream0_count
6022                .saturating_add(1);
6023            debug_assert!(
6024                self.flood_detector.window_update_stream0_count > wu0_before
6025                    || wu0_before == u32::MAX,
6026                "stream-0 WINDOW_UPDATE flood counter must advance before the flood check"
6027            );
6028            check_flood_or_return!(self);
6029            self.attribute_bytes_to_overhead();
6030            let window_before = self.flow_control.window;
6031            if let Some(window) = self.flow_control.window.checked_add(increment) {
6032                if self.flow_control.window <= 0 && window > 0 {
6033                    self.readiness.arm_writable();
6034                }
6035                self.flow_control.window = window;
6036                // Flow-control replenish invariant (RFC 9113 §6.9): the
6037                // connection send window grows by exactly `increment` and stays
6038                // within i32 (the `checked_add` already rejected overflow, which
6039                // is a FLOW_CONTROL_ERROR on the wire). The window may legally
6040                // be negative (a SETTINGS change can shrink it below zero) but
6041                // a WINDOW_UPDATE only ever increases it.
6042                debug_assert_eq!(
6043                    self.flow_control.window,
6044                    window_before + increment,
6045                    "connection window must increase by exactly the increment"
6046                );
6047                debug_assert!(
6048                    self.flow_control.window > window_before,
6049                    "a positive WINDOW_UPDATE must strictly grow the connection window"
6050                );
6051                debug!(
6052                    "{} WINDOW_UPDATE received: stream=0 increment={} new_connection_window={}",
6053                    log_context!(self),
6054                    increment,
6055                    self.flow_control.window
6056                );
6057            } else {
6058                error!("{} INVALID WINDOW INCREMENT", log_context!(self));
6059                return self.goaway(H2Error::FlowControlError);
6060            }
6061        } else if let Some(global_stream_id) = self.streams.get(&stream_id).copied() {
6062            let stream = &mut context.streams[global_stream_id];
6063            self.attribute_bytes_to_stream(&mut stream.metrics);
6064            let stream_window_before = stream.window;
6065            if let Some(window) = stream.window.checked_add(increment) {
6066                if stream.window <= 0 && window > 0 {
6067                    self.readiness.arm_writable();
6068                }
6069                stream.window = window;
6070                // Same replenish invariant as the connection window, applied to
6071                // the per-stream send window (RFC 9113 §6.9.1). Overflow past
6072                // 2^31-1 is rejected by `checked_add` and handled as a
6073                // FLOW_CONTROL_ERROR RST_STREAM below.
6074                debug_assert_eq!(
6075                    stream.window,
6076                    stream_window_before + increment,
6077                    "stream window must increase by exactly the increment"
6078                );
6079                debug_assert!(
6080                    stream.window > stream_window_before,
6081                    "a positive WINDOW_UPDATE must strictly grow the stream window"
6082                );
6083                debug!(
6084                    "{} WINDOW_UPDATE received: stream={} increment={} new_stream_window={}",
6085                    log_context!(self),
6086                    stream_id,
6087                    increment,
6088                    stream.window
6089                );
6090            } else {
6091                let result = self.reset_stream(
6092                    stream_id,
6093                    global_stream_id,
6094                    context,
6095                    endpoint,
6096                    H2Error::FlowControlError,
6097                );
6098                self.remove_dead_stream(stream_id, global_stream_id);
6099                return result;
6100            }
6101        } else {
6102            self.attribute_bytes_to_overhead();
6103            trace!(
6104                "{} Ignoring window update on closed stream {}: {}",
6105                log_context!(self),
6106                stream_id,
6107                increment
6108            );
6109            // Pass 3 Low #5: WINDOW_UPDATE on a closed stream is legal
6110            // (RFC 9113 §6.9.1) but has no useful effect, so a peer that
6111            // keeps sending them is wasting our cycles. Count it as a
6112            // glitch so a flood contributes to `check_flood()` and can
6113            // eventually trigger ENHANCE_YOUR_CALM.
6114            self.flood_detector.glitch_count += 1;
6115            check_flood_or_return!(self);
6116        }
6117        MuxResult::Continue
6118    }
6119
6120    fn update_initial_window_size<L>(&mut self, value: u32, context: &mut Context<L>) -> bool
6121    where
6122        L: ListenerHandler + L7ListenerHandler,
6123    {
6124        if value > FLOW_CONTROL_MAX_WINDOW {
6125            return true;
6126        }
6127        let delta = match i32::try_from(
6128            value as i64 - self.peer_settings.settings_initial_window_size as i64,
6129        ) {
6130            Ok(d) => d,
6131            Err(_) => {
6132                error!("{} initial window size delta overflow", log_context!(self));
6133                return true;
6134            }
6135        };
6136        let mut open_window = false;
6137        // Only update windows for streams owned by this connection
6138        for &global_stream_id in self.streams.values() {
6139            let stream = &mut context.streams[global_stream_id];
6140            // RFC 9113 §6.9.2: changes to SETTINGS_INITIAL_WINDOW_SIZE can cause
6141            // stream windows to exceed 2^31-1, which is a flow control error.
6142            match stream.window.checked_add(delta) {
6143                Some(new_window) => {
6144                    open_window |= stream.window <= 0 && new_window > 0;
6145                    stream.window = new_window;
6146                }
6147                None => return true,
6148            }
6149        }
6150        trace!(
6151            "{} UPDATE INIT WINDOW: {} {} {:?}",
6152            log_context!(self),
6153            delta,
6154            open_window,
6155            self.readiness
6156        );
6157        if open_window {
6158            self.readiness.arm_writable();
6159        }
6160        self.peer_settings.settings_initial_window_size = value;
6161        false
6162    }
6163
6164    pub fn force_disconnect(&mut self) -> MuxResult {
6165        self.state = H2State::Error;
6166        match &mut self.position {
6167            Position::Client(_, _, status) => {
6168                *status = BackendStatus::Disconnecting;
6169                self.readiness.event = Ready::HUP;
6170                debug!(
6171                    "{} H2 force_disconnect client: state={:?}, streams={}, expect_write={:?}, wants_write={}, readiness={:?}",
6172                    log_context!(self),
6173                    self.state,
6174                    self.streams.len(),
6175                    self.expect_write,
6176                    self.socket.socket_wants_write(),
6177                    self.readiness
6178                );
6179                MuxResult::Continue
6180            }
6181            Position::Server => {
6182                if self.peer_gone_after_final_goaway() {
6183                    return MuxResult::CloseSession;
6184                }
6185                // Don't disconnect immediately if rustls still has buffered TLS
6186                // records. Returning CloseSession here triggers shutdown(Write)
6187                // which sends FIN — but any TLS records still in rustls's buffer
6188                // (not yet flushed to the TCP send buffer) are lost, causing the
6189                // client to see "TLS decode error / unexpected eof".
6190                // Instead, keep WRITABLE interest and let the writable path flush.
6191                if self.socket.socket_wants_write() {
6192                    debug!(
6193                        "{} H2 force_disconnect delaying close: state={:?}, streams={}, expect_write={:?}, wants_write=true, readiness={:?}",
6194                        log_context!(self),
6195                        self.state,
6196                        self.streams.len(),
6197                        self.expect_write,
6198                        self.readiness
6199                    );
6200                    self.readiness.interest = Ready::WRITABLE | Ready::HUP | Ready::ERROR;
6201                    self.ensure_tls_flushed();
6202                    MuxResult::Continue
6203                } else {
6204                    debug!(
6205                        "{} H2 force_disconnect closing session: state={:?}, streams={}, expect_write={:?}, wants_write=false, readiness={:?}",
6206                        log_context!(self),
6207                        self.state,
6208                        self.streams.len(),
6209                        self.expect_write,
6210                        self.readiness
6211                    );
6212                    MuxResult::CloseSession
6213                }
6214            }
6215        }
6216    }
6217
6218    pub fn close<E, L>(&mut self, context: &mut Context<L>, mut endpoint: E)
6219    where
6220        E: Endpoint,
6221        L: ListenerHandler + L7ListenerHandler,
6222    {
6223        match self.position {
6224            Position::Client(_, _, BackendStatus::KeepAlive) => {
6225                error!(
6226                    "{} H2 connections do not use KeepAlive backend status",
6227                    log_context!(self)
6228                );
6229                return;
6230            }
6231            Position::Client(..) => {}
6232            Position::Server => {
6233                let tls_pending_before = self.socket.socket_wants_write();
6234                if !self.streams.is_empty() || tls_pending_before || self.expect_write.is_some() {
6235                    debug!(
6236                        "{} H2 close with active state: state={:?}, streams={}, expect_write={:?}, wants_write={}, readiness={:?}",
6237                        log_context!(self),
6238                        self.state,
6239                        self.streams.len(),
6240                        self.expect_write,
6241                        tls_pending_before,
6242                        self.readiness
6243                    );
6244                    for (stream_id, global_stream_id) in &self.streams {
6245                        let stream = &context.streams[*global_stream_id];
6246                        debug!(
6247                            "{}   close stream id={} gid={}: state={:?}, front_eos={}, back_eos={}, front_phase={:?}, back_phase={:?}, front_completed={}, back_completed={}",
6248                            log_context!(self),
6249                            stream_id,
6250                            global_stream_id,
6251                            stream.state,
6252                            stream.front_received_end_of_stream,
6253                            stream.back_received_end_of_stream,
6254                            stream.front.parsing_phase,
6255                            stream.back.parsing_phase,
6256                            stream.front.is_completed(),
6257                            stream.back.is_completed()
6258                        );
6259                    }
6260                }
6261                if !self.close_notify_sent {
6262                    trace!("{} H2 SENDING CLOSE NOTIFY", log_context!(self));
6263                }
6264                let (tls_pending_after, drain_rounds) =
6265                    drain_tls_close_notify(&mut self.socket, &mut self.close_notify_sent);
6266                if tls_pending_after {
6267                    // Severity tiering: key on stream-count + close-state, not
6268                    // peer-vs-operator. Composes with the send-side `H2Error`
6269                    // variant tier in `goaway()` — both rules demote benign
6270                    // paths and keep loss-bearing paths loud.
6271                    //
6272                    // - `streams != 0`           -> `error!`: live streams at
6273                    //   close time, response-byte loss is possible.
6274                    // - `streams == 0` AND state in {GoAway, Error}
6275                    //                             -> `warn!`: idle close after
6276                    //   a GOAWAY exchange (peer-initiated abort or our own
6277                    //   graceful drain). What's stranded is best-effort
6278                    //   GOAWAY/close_notify; no application data was queued.
6279                    // - `streams == 0` from any other state
6280                    //                             -> `error!`: unexpected
6281                    //   teardown path (no GOAWAY exchange) — keep loud so
6282                    //   unknown failure modes surface.
6283                    if !self.streams.is_empty() {
6284                        error!(
6285                            "{} TLS buffer NOT fully drained on close: \
6286                             pending_before={}, pending_after={}, drain_rounds={}, \
6287                             state={:?}, streams={}, expect_write={:?}, \
6288                             close_notify_sent={}, readiness={:?}",
6289                            log_context!(self),
6290                            tls_pending_before,
6291                            tls_pending_after,
6292                            drain_rounds,
6293                            self.state,
6294                            self.streams.len(),
6295                            self.expect_write,
6296                            self.close_notify_sent,
6297                            self.readiness
6298                        );
6299                    } else if matches!(self.state, H2State::GoAway | H2State::Error) {
6300                        warn!(
6301                            "{} TLS buffer NOT fully drained on close: \
6302                             pending_before={}, pending_after={}, drain_rounds={}, \
6303                             state={:?}, streams={}, expect_write={:?}, \
6304                             close_notify_sent={}, readiness={:?}",
6305                            log_context!(self),
6306                            tls_pending_before,
6307                            tls_pending_after,
6308                            drain_rounds,
6309                            self.state,
6310                            self.streams.len(),
6311                            self.expect_write,
6312                            self.close_notify_sent,
6313                            self.readiness
6314                        );
6315                    } else {
6316                        error!(
6317                            "{} TLS buffer NOT fully drained on close: \
6318                             pending_before={}, pending_after={}, drain_rounds={}, \
6319                             state={:?}, streams={}, expect_write={:?}, \
6320                             close_notify_sent={}, readiness={:?}",
6321                            log_context!(self),
6322                            tls_pending_before,
6323                            tls_pending_after,
6324                            drain_rounds,
6325                            self.state,
6326                            self.streams.len(),
6327                            self.expect_write,
6328                            self.close_notify_sent,
6329                            self.readiness
6330                        );
6331                    }
6332                }
6333                return;
6334            }
6335        }
6336        // reconnection is handled by the server for each stream separately
6337        for global_stream_id in self.streams.values() {
6338            trace!("{} end stream: {}", log_context!(self), global_stream_id);
6339            if let StreamState::Linked(token) = context.streams[*global_stream_id].state {
6340                endpoint.end_stream(token, *global_stream_id, context);
6341            }
6342        }
6343    }
6344
6345    /// Reset a stream: tear down kawa state, emit `RST_STREAM` on the wire,
6346    /// and record MadeYouReset accounting.
6347    ///
6348    /// `wire_stream_id` is the on-wire `StreamId`; `stream_id` is the internal
6349    /// `GlobalStreamId` slot. Callers already carry both so we pass them
6350    /// explicitly rather than scanning `self.streams`. The wire id is threaded
6351    /// into [`Self::enqueue_rst`] which queues the frame for serialisation in
6352    /// [`Self::flush_pending_control_frames`] on the next writable tick —
6353    /// independent of whether the caller immediately evicts the slot via
6354    /// `remove_dead_stream` (which they usually do). This is what guarantees
6355    /// the RST reaches the peer for malformed HEADERS / flow-control /
6356    /// content-length violations flagged by h2spec 2.0.
6357    pub fn reset_stream<E, L>(
6358        &mut self,
6359        wire_stream_id: StreamId,
6360        stream_id: GlobalStreamId,
6361        context: &mut Context<L>,
6362        mut endpoint: E,
6363        error: H2Error,
6364    ) -> MuxResult
6365    where
6366        E: Endpoint,
6367        L: ListenerHandler + L7ListenerHandler,
6368    {
6369        // Compute totals before taking mutable borrows on the target stream.
6370        let reset_byte_totals = self.compute_stream_byte_totals(context);
6371        context.unlink_stream(stream_id);
6372        let stream = &mut context.streams[stream_id];
6373        trace!(
6374            "{} reset H2 stream {}: {:#?}",
6375            log_context!(self),
6376            stream_id,
6377            stream.context
6378        );
6379        let old_state = std::mem::replace(&mut stream.state, StreamState::Unlinked);
6380        forcefully_terminate_answer(stream, &mut self.readiness, error);
6381        let linked_token = if let StreamState::Linked(token) = old_state {
6382            Some(token)
6383        } else {
6384            None
6385        };
6386        let (client_rtt, server_rtt) =
6387            Self::snapshot_rtts(&self.position, &self.socket, &endpoint, linked_token);
6388        if let Some(token) = linked_token {
6389            endpoint.end_stream(token, stream_id, context);
6390        }
6391        // Emit access log for server-side resets on streams that had active requests
6392        if self.position.is_server()
6393            && matches!(old_state, StreamState::Link | StreamState::Linked(_))
6394        {
6395            let stream = &mut context.streams[stream_id];
6396            self.distribute_overhead(&mut stream.metrics, reset_byte_totals);
6397            stream.metrics.backend_stop();
6398            stream.generate_access_log(
6399                true,
6400                Some("H2::Reset"),
6401                context.listener.clone(),
6402                client_rtt,
6403                server_rtt,
6404            );
6405            stream.metrics.reset();
6406        }
6407        // Queue the RST for wire emission. Independent of the owning stream
6408        // remaining in `self.streams` — callers typically follow this with
6409        // `remove_dead_stream`, which would otherwise evict the slot before
6410        // `write_streams` could run `kawa.prepare` against the converter.
6411        //
6412        // `enqueue_rst` performs every accounting side-effect at queue
6413        // time (per-error counter, global tx counter, CVE-2025-8671
6414        // MadeYouReset lifetime cap). Graceful `NoError` cancels —
6415        // stream recycle, propagated client-side cancel — are exempt
6416        // from the lifetime cap inside the accounting helper itself.
6417        if let Some(result) = self.enqueue_rst(wire_stream_id, error) {
6418            return result;
6419        }
6420        MuxResult::Continue
6421    }
6422
6423    pub fn end_stream<L>(&mut self, stream_gid: GlobalStreamId, context: &mut Context<L>)
6424    where
6425        L: ListenerHandler + L7ListenerHandler,
6426    {
6427        context.unlink_stream(stream_gid);
6428        let stream_context = context.http_context(stream_gid);
6429        trace!(
6430            "{} end H2 stream {}: {:#?}",
6431            log_context!(self),
6432            stream_gid,
6433            stream_context
6434        );
6435        match self.position {
6436            Position::Client(..) => {
6437                // Resolve the wire StreamId for this gid up front so the
6438                // subsequent cleanup does not hold an iterator borrow on
6439                // `self.streams` while also mutating it.
6440                let wire_stream_id = self
6441                    .streams
6442                    .iter()
6443                    .find_map(|(&sid, &gid)| (gid == stream_gid).then_some(sid));
6444                if let Some(id) = wire_stream_id {
6445                    // Only send RST_STREAM if the stream hasn't fully completed.
6446                    // If both request and response are terminated, the stream is
6447                    // already in "closed" state (RFC 9113 §5.1) — sending RST_STREAM
6448                    // on a closed stream would be a protocol error that could cause
6449                    // the H2 peer to close the entire connection.
6450                    let stream = &context.streams[stream_gid];
6451                    let fully_completed =
6452                        stream.back_received_end_of_stream && stream.front.is_terminated();
6453                    if !fully_completed && !self.rst_sent.contains(&id) {
6454                        let kawa = &mut self.zero;
6455                        let mut frame = [0; 13];
6456                        if let Ok((_, _size)) =
6457                            serializer::gen_rst_stream(&mut frame, id, H2Error::Cancel)
6458                        {
6459                            let buf = kawa.storage.space();
6460                            if buf.len() >= frame.len() {
6461                                buf[..frame.len()].copy_from_slice(&frame);
6462                                kawa.storage.fill(frame.len());
6463                                incr!(names::h2::FRAMES_TX_RST_STREAM);
6464                                count!(metric_for_rst_stream_sent(H2Error::Cancel), 1);
6465                                self.readiness.arm_writable();
6466                                self.rst_sent.insert(id);
6467                            }
6468                        }
6469                    }
6470                    // Retire the stream and invalidate expect_write/expect_read
6471                    // if they still reference this gid — the slot may be popped
6472                    // by `shrink_trailing_recycle` on the next create_stream.
6473                    self.remove_dead_stream(id, stream_gid);
6474                    if context.streams[stream_gid].state != StreamState::Recycle {
6475                        context.streams[stream_gid].state = StreamState::Unlinked;
6476                    }
6477                    return;
6478                }
6479                error!(
6480                    "{} end_stream called for unknown global_stream_id {}",
6481                    log_context!(self),
6482                    stream_gid
6483                );
6484            }
6485            Position::Server => {
6486                let answers_rc = context.listener.borrow().get_answers().clone();
6487                let stream = &mut context.streams[stream_gid];
6488                match end_stream_decision(stream) {
6489                    EndStreamAction::ForwardTerminated => {
6490                        #[cfg(debug_assertions)]
6491                        context
6492                            .debug
6493                            .push(DebugEvent::Str(format!("Close terminated {stream_gid}")));
6494                        debug!(
6495                            "{} CLOSING H2 TERMINATED STREAM {} {:?}",
6496                            log_context!(self),
6497                            stream_gid,
6498                            stream
6499                        );
6500                        stream.state = StreamState::Unlinked;
6501                        self.readiness.arm_writable();
6502                        context.debug.set_interesting(true);
6503                    }
6504                    EndStreamAction::CloseDelimited => {
6505                        debug!(
6506                            "{} CLOSE DELIMITED H2 STREAM {} {:?}",
6507                            log_context!(self),
6508                            stream_gid,
6509                            stream
6510                        );
6511                        stream.back.push_block(kawa::Block::Flags(kawa::Flags {
6512                            end_body: true,
6513                            end_chunk: false,
6514                            end_header: false,
6515                            end_stream: true,
6516                        }));
6517                        stream.back.parsing_phase = kawa::ParsingPhase::Terminated;
6518                        stream.state = StreamState::Unlinked;
6519                        self.readiness.arm_writable();
6520                        context.debug.set_interesting(true);
6521                    }
6522                    EndStreamAction::ForwardUnterminated => {
6523                        #[cfg(debug_assertions)]
6524                        context
6525                            .debug
6526                            .push(DebugEvent::Str(format!("Close unterminated {stream_gid}")));
6527                        debug!(
6528                            "{} CLOSING H2 UNTERMINATED STREAM {} {:?}",
6529                            log_context!(self),
6530                            stream_gid,
6531                            stream
6532                        );
6533                        forcefully_terminate_answer(
6534                            stream,
6535                            &mut self.readiness,
6536                            H2Error::InternalError,
6537                        );
6538                        context.debug.set_interesting(true);
6539                    }
6540                    EndStreamAction::SendDefault(status) => {
6541                        #[cfg(debug_assertions)]
6542                        context.debug.push(DebugEvent::Str(format!(
6543                            "Can't retry, send {status} on {stream_gid}"
6544                        )));
6545                        let answers = answers_rc.borrow();
6546                        set_default_answer(stream, &mut self.readiness, status, &answers);
6547                    }
6548                    EndStreamAction::Reconnect => {
6549                        debug!("{} H2 RECONNECT", log_context!(self));
6550                        #[cfg(debug_assertions)]
6551                        context
6552                            .debug
6553                            .push(DebugEvent::Str(format!("Retry {stream_gid}")));
6554                        stream.state = StreamState::Link;
6555                        context.pending_links.push_back(stream_gid);
6556                    }
6557                }
6558            }
6559        }
6560    }
6561
6562    pub fn start_stream<L>(&mut self, stream: GlobalStreamId, _context: &mut Context<L>) -> bool
6563    where
6564        L: ListenerHandler + L7ListenerHandler,
6565    {
6566        // RFC 9113 §6.8: reject new streams on a draining connection
6567        if self.drain.draining {
6568            error!(
6569                "{} Cannot open new stream on draining connection (stream {})",
6570                log_context!(self),
6571                stream
6572            );
6573            return false;
6574        }
6575        // RFC 9113 §5.1.2: respect peer's max concurrent streams limit
6576        if self.streams.len() >= self.peer_settings.settings_max_concurrent_streams as usize {
6577            error!(
6578                "{} Cannot open new stream: active={} >= peer max_concurrent_streams={}",
6579                log_context!(self),
6580                self.streams.len(),
6581                self.peer_settings.settings_max_concurrent_streams
6582            );
6583            return false;
6584        }
6585        trace!(
6586            "{} start new H2 stream {} {:?}",
6587            log_context!(self),
6588            stream,
6589            self.readiness
6590        );
6591        let Some(stream_id) = self.new_stream_id() else {
6592            // Pass 4 Medium #5: the client-initiated stream-ID space
6593            // (31 bits, odd only) is exhausted. The backend is now useless
6594            // for new requests — gracefully drain it. Without this
6595            // transition, the Connection lingers in `Connected` state and
6596            // every subsequent request returns 503 because `start_stream`
6597            // keeps returning false.
6598            //
6599            // The session envelope is hoisted to a local because the
6600            // `match &mut self.position` below holds a mutable borrow on
6601            // `self.position`, and `log_context!(self)` reads that field
6602            // for its `position={...}` slot — calling the macro inside the
6603            // match arms would conflict with the active borrow. The
6604            // bidirectional regression guard in `lib/tests/log_layout.rs`
6605            // (and the matching scanner in `lib/build.rs`) recognises this
6606            // shape by scanning backward as well as forward from each log
6607            // call.
6608            let context = log_context!(self);
6609            match &mut self.position {
6610                Position::Client(cluster_id, backend, status) => {
6611                    let backend_addr = backend.borrow().address;
6612                    let cluster = cluster_id.clone();
6613                    info!(
6614                        "{} H2 backend stream IDs exhausted (cluster={}, backend={:?}) — draining",
6615                        context, cluster, backend_addr
6616                    );
6617                    *status = BackendStatus::Disconnecting;
6618                }
6619                Position::Server => {
6620                    error!(
6621                        "{} H2 server stream IDs exhausted — sending graceful GOAWAY",
6622                        context
6623                    );
6624                }
6625            }
6626            self.graceful_goaway();
6627            return false;
6628        };
6629        self.streams.insert(stream_id, stream);
6630        self.stream_last_activity_at
6631            .insert(stream_id, Instant::now());
6632        self.readiness.arm_writable();
6633        true
6634    }
6635}
6636
6637#[cfg(test)]
6638mod tests {
6639    use std::{cell::RefCell, rc::Rc};
6640
6641    use super::*;
6642    use crate::{pool::Pool, protocol::kawa_h1::editor::HttpContext};
6643
6644    // ── H2FloodDetector ──────────────────────────────────────────────────
6645
6646    #[test]
6647    fn test_flood_detector_no_flood_below_threshold() {
6648        let config = H2FloodConfig::default();
6649        let mut detector = H2FloodDetector::new(config);
6650
6651        // All counters at zero -> no flood
6652        assert!(detector.check_flood().is_none());
6653
6654        // Increment each counter to exactly the threshold (not exceeding)
6655        detector.rst_stream_count = config.max_rst_stream_per_window;
6656        detector.ping_count = config.max_ping_per_window;
6657        detector.settings_count = config.max_settings_per_window;
6658        detector.empty_data_count = config.max_empty_data_per_window;
6659        detector.continuation_count = config.max_continuation_frames;
6660        detector.glitch_count = config.max_glitch_count;
6661        // At threshold but not exceeding -> no flood
6662        assert!(detector.check_flood().is_none());
6663    }
6664
6665    #[test]
6666    fn test_flood_detector_detects_rapid_reset() {
6667        let config = H2FloodConfig::default();
6668        let mut detector = H2FloodDetector::new(config);
6669
6670        detector.rst_stream_count = config.max_rst_stream_per_window + 1;
6671        assert!(matches!(
6672            detector.check_flood(),
6673            Some(H2FloodViolation {
6674                error: H2Error::EnhanceYourCalm,
6675                ..
6676            })
6677        ));
6678    }
6679
6680    #[test]
6681    fn test_flood_detector_detects_ping_flood() {
6682        let config = H2FloodConfig::default();
6683        let mut detector = H2FloodDetector::new(config);
6684
6685        detector.ping_count = config.max_ping_per_window + 1;
6686        assert!(matches!(
6687            detector.check_flood(),
6688            Some(H2FloodViolation {
6689                error: H2Error::EnhanceYourCalm,
6690                ..
6691            })
6692        ));
6693    }
6694
6695    #[test]
6696    fn test_flood_detector_detects_settings_flood() {
6697        let config = H2FloodConfig::default();
6698        let mut detector = H2FloodDetector::new(config);
6699
6700        detector.settings_count = config.max_settings_per_window + 1;
6701        assert!(matches!(
6702            detector.check_flood(),
6703            Some(H2FloodViolation {
6704                error: H2Error::EnhanceYourCalm,
6705                ..
6706            })
6707        ));
6708    }
6709
6710    #[test]
6711    fn test_flood_detector_detects_empty_data_flood() {
6712        let config = H2FloodConfig::default();
6713        let mut detector = H2FloodDetector::new(config);
6714
6715        detector.empty_data_count = config.max_empty_data_per_window + 1;
6716        assert!(matches!(
6717            detector.check_flood(),
6718            Some(H2FloodViolation {
6719                error: H2Error::EnhanceYourCalm,
6720                ..
6721            })
6722        ));
6723    }
6724
6725    #[test]
6726    fn test_flood_detector_detects_continuation_flood() {
6727        let config = H2FloodConfig::default();
6728        let mut detector = H2FloodDetector::new(config);
6729
6730        detector.continuation_count = config.max_continuation_frames + 1;
6731        assert!(matches!(
6732            detector.check_flood(),
6733            Some(H2FloodViolation {
6734                error: H2Error::EnhanceYourCalm,
6735                ..
6736            })
6737        ));
6738    }
6739
6740    #[test]
6741    fn test_flood_detector_detects_header_size_flood() {
6742        let config = H2FloodConfig::default();
6743        let mut detector = H2FloodDetector::new(config);
6744
6745        detector.accumulated_header_size = MAX_HEADER_LIST_SIZE as u32 + 1;
6746        assert!(matches!(
6747            detector.check_flood(),
6748            Some(H2FloodViolation {
6749                error: H2Error::EnhanceYourCalm,
6750                ..
6751            })
6752        ));
6753    }
6754
6755    #[test]
6756    fn test_flood_detector_detects_glitch_flood() {
6757        let config = H2FloodConfig::default();
6758        let mut detector = H2FloodDetector::new(config);
6759
6760        detector.glitch_count = config.max_glitch_count + 1;
6761        assert!(matches!(
6762            detector.check_flood(),
6763            Some(H2FloodViolation {
6764                error: H2Error::EnhanceYourCalm,
6765                ..
6766            })
6767        ));
6768    }
6769
6770    #[test]
6771    fn test_flood_detector_custom_thresholds() {
6772        let config = H2FloodConfig {
6773            max_rst_stream_per_window: 5,
6774            max_ping_per_window: 10,
6775            max_settings_per_window: 3,
6776            max_empty_data_per_window: 8,
6777            max_continuation_frames: 2,
6778            max_glitch_count: 15,
6779            ..H2FloodConfig::default()
6780        };
6781        let mut detector = H2FloodDetector::new(config);
6782
6783        // Below custom threshold -> no flood
6784        detector.rst_stream_count = 5;
6785        assert!(detector.check_flood().is_none());
6786
6787        // Above custom threshold -> flood
6788        detector.rst_stream_count = 6;
6789        assert!(matches!(
6790            detector.check_flood(),
6791            Some(H2FloodViolation {
6792                error: H2Error::EnhanceYourCalm,
6793                ..
6794            })
6795        ));
6796    }
6797
6798    #[test]
6799    fn test_flood_detector_reset_continuation() {
6800        let config = H2FloodConfig::default();
6801        let mut detector = H2FloodDetector::new(config);
6802
6803        detector.continuation_count = 15;
6804        detector.accumulated_header_size = 30000;
6805
6806        detector.reset_continuation();
6807
6808        assert_eq!(detector.continuation_count, 0);
6809        assert_eq!(detector.accumulated_header_size, 0);
6810    }
6811
6812    #[test]
6813    fn test_flood_detector_half_decay_on_window_expiry() {
6814        let config = H2FloodConfig::default();
6815        let mut detector = H2FloodDetector::new(config);
6816
6817        detector.rst_stream_count = 80;
6818        detector.ping_count = 60;
6819        detector.settings_count = 40;
6820        detector.empty_data_count = 20;
6821        detector.window_update_stream0_count = 90;
6822        detector.glitch_count = 50;
6823
6824        // Force window expiry by setting window_start to the past
6825        detector.window_start = Instant::now() - FLOOD_WINDOW_DURATION;
6826
6827        // check_flood calls maybe_reset_window which halves counters
6828        let _ = detector.check_flood();
6829
6830        assert_eq!(detector.rst_stream_count, 40);
6831        assert_eq!(detector.ping_count, 30);
6832        assert_eq!(detector.settings_count, 20);
6833        assert_eq!(detector.empty_data_count, 10);
6834        assert_eq!(detector.window_update_stream0_count, 45);
6835        assert_eq!(detector.glitch_count, 25);
6836    }
6837
6838    #[test]
6839    fn test_flood_detector_window_update_stream0_trips_at_threshold() {
6840        let config = H2FloodConfig {
6841            max_window_update_stream0_per_window: 5,
6842            ..H2FloodConfig::default()
6843        };
6844        let mut detector = H2FloodDetector::new(config);
6845
6846        // At threshold — no flood yet (strict greater-than, matches existing counters).
6847        detector.window_update_stream0_count = 5;
6848        assert!(detector.check_flood().is_none());
6849
6850        // Above threshold — flood with the correct violation reason + metric key.
6851        detector.window_update_stream0_count = 6;
6852        let violation = detector
6853            .check_flood()
6854            .expect("WINDOW_UPDATE stream-0 flood must trip above threshold");
6855        assert_eq!(violation.error, H2Error::EnhanceYourCalm);
6856        assert_eq!(violation.reason, "WINDOW_UPDATE stream 0");
6857        assert_eq!(
6858            violation.metric_key,
6859            "h2.flood.violation.window_update_stream0_window"
6860        );
6861        assert_eq!(violation.count, 6);
6862        assert_eq!(violation.threshold, 5);
6863    }
6864
6865    #[test]
6866    fn test_flood_detector_window_update_stream0_honours_default() {
6867        // Default threshold must match the documented constant so operators
6868        // can reason about behaviour without reading code.
6869        let detector = H2FloodDetector::default();
6870        assert_eq!(
6871            detector.config.max_window_update_stream0_per_window,
6872            DEFAULT_MAX_WINDOW_UPDATE_STREAM0_PER_WINDOW
6873        );
6874        assert_eq!(detector.window_update_stream0_count, 0);
6875    }
6876
6877    #[test]
6878    fn test_flood_detector_decay_prevents_flood() {
6879        let config = H2FloodConfig {
6880            max_rst_stream_per_window: 10,
6881            ..H2FloodConfig::default()
6882        };
6883        let mut detector = H2FloodDetector::new(config);
6884
6885        // Set counter just above threshold
6886        detector.rst_stream_count = 12;
6887
6888        // Without decay -> flood
6889        assert!(matches!(
6890            detector.check_flood(),
6891            Some(H2FloodViolation {
6892                error: H2Error::EnhanceYourCalm,
6893                ..
6894            })
6895        ));
6896
6897        // Reset and simulate window expiry
6898        detector.rst_stream_count = 12;
6899        detector.window_start = Instant::now() - FLOOD_WINDOW_DURATION;
6900
6901        // After decay: 12/2 = 6, which is below threshold 10 -> no flood
6902        assert!(detector.check_flood().is_none());
6903    }
6904
6905    #[test]
6906    fn test_flood_detector_lifetime_rst_cap_triggers_enhance_your_calm() {
6907        // CVE-2023-44487 Rapid Reset: a patient attacker that stays under
6908        // the half-decaying per-window threshold must still be stopped by
6909        // the lifetime cap. Simulate a response-started RST (no abusive
6910        // counter bump) so only the lifetime ceiling is tested.
6911        let mut detector = H2FloodDetector::default();
6912        for _ in 0..DEFAULT_MAX_RST_STREAM_LIFETIME {
6913            assert!(detector.record_rst_lifetime(true).is_none());
6914        }
6915        assert_eq!(
6916            detector.total_rst_received_lifetime,
6917            DEFAULT_MAX_RST_STREAM_LIFETIME
6918        );
6919        assert_eq!(detector.total_abusive_rst_received_lifetime, 0);
6920        // Next RST crosses the ceiling.
6921        assert!(matches!(
6922            detector.record_rst_lifetime(true),
6923            Some(H2FloodViolation {
6924                error: H2Error::EnhanceYourCalm,
6925                ..
6926            })
6927        ));
6928    }
6929
6930    #[test]
6931    fn test_flood_detector_abusive_rst_cap_triggers_first() {
6932        // Pre-response-start RSTs have a much lower ceiling; they trip
6933        // well before the generic lifetime cap.
6934        let mut detector = H2FloodDetector::default();
6935        for _ in 0..DEFAULT_MAX_RST_STREAM_ABUSIVE_LIFETIME {
6936            assert!(detector.record_rst_lifetime(false).is_none());
6937        }
6938        assert_eq!(
6939            detector.total_abusive_rst_received_lifetime,
6940            DEFAULT_MAX_RST_STREAM_ABUSIVE_LIFETIME
6941        );
6942        assert!(matches!(
6943            detector.record_rst_lifetime(false),
6944            Some(H2FloodViolation {
6945                error: H2Error::EnhanceYourCalm,
6946                ..
6947            })
6948        ));
6949    }
6950
6951    #[test]
6952    fn test_flood_detector_emitted_rst_below_threshold_is_clean() {
6953        // Server may legitimately RST some streams (protocol errors,
6954        // client-side abuse caught by other mitigations). Staying at the
6955        // threshold must not trip the ceiling.
6956        let mut detector = H2FloodDetector::default();
6957        for _ in 0..DEFAULT_MAX_RST_STREAM_EMITTED_LIFETIME {
6958            assert!(detector.record_rst_emitted().is_none());
6959        }
6960        assert_eq!(
6961            detector.total_rst_streams_emitted_lifetime,
6962            DEFAULT_MAX_RST_STREAM_EMITTED_LIFETIME
6963        );
6964    }
6965
6966    #[test]
6967    fn test_flood_detector_emitted_rst_cap_triggers_made_you_reset() {
6968        // CVE-2025-8671 MadeYouReset: unbounded server-emitted RST_STREAM is
6969        // a DoS vector equivalent to Rapid Reset with the emission direction
6970        // flipped. Crossing the ceiling must surface a EnhanceYourCalm
6971        // violation so the caller can GOAWAY.
6972        let mut detector = H2FloodDetector::default();
6973        for _ in 0..DEFAULT_MAX_RST_STREAM_EMITTED_LIFETIME {
6974            assert!(detector.record_rst_emitted().is_none());
6975        }
6976        let violation = detector
6977            .record_rst_emitted()
6978            .expect("emitting past the cap should produce a violation");
6979        assert!(matches!(
6980            violation,
6981            H2FloodViolation {
6982                error: H2Error::EnhanceYourCalm,
6983                reason: "MadeYouReset: lifetime server-emitted RST_STREAM",
6984                ..
6985            }
6986        ));
6987        assert_eq!(violation.count, DEFAULT_MAX_RST_STREAM_EMITTED_LIFETIME + 1);
6988        assert_eq!(violation.threshold, DEFAULT_MAX_RST_STREAM_EMITTED_LIFETIME);
6989    }
6990
6991    #[test]
6992    fn test_flood_detector_emitted_rst_counter_does_not_decay() {
6993        // Unlike the windowed rst_stream_count, the emitted lifetime counter
6994        // is strictly monotonic — a patient attacker cannot reset it by
6995        // waiting out a window. maybe_reset_window must NOT touch it.
6996        let mut detector = H2FloodDetector::default();
6997        for _ in 0..10 {
6998            detector.record_rst_emitted();
6999        }
7000        detector.window_start = Instant::now() - FLOOD_WINDOW_DURATION;
7001        // Force a window reset through check_flood.
7002        let _ = detector.check_flood();
7003        assert_eq!(detector.total_rst_streams_emitted_lifetime, 10);
7004    }
7005
7006    /// Every violation kind must carry a metric_key under the agreed
7007    /// `h2.flood.violation.*` namespace, and the keys must be unique. The
7008    /// statsd counter at `handle_flood_violation` reads `violation.metric_key`
7009    /// directly — drift between the construction site and the metric name
7010    /// would silently lose alerting on a CVE mitigation.
7011    #[test]
7012    fn test_flood_violation_metric_keys_are_unique_and_namespaced() {
7013        // Helper: run `record_rst_lifetime` until it trips, returning the metric_key.
7014        fn key_from_rst_lifetime(response_started: bool) -> &'static str {
7015            let mut detector = H2FloodDetector::default();
7016            loop {
7017                if let Some(v) = detector.record_rst_lifetime(response_started) {
7018                    return v.metric_key;
7019                }
7020            }
7021        }
7022
7023        // Helper: run `record_rst_emitted` until it trips, returning the metric_key.
7024        fn key_from_rst_emitted() -> &'static str {
7025            let mut detector = H2FloodDetector::default();
7026            loop {
7027                if let Some(v) = detector.record_rst_emitted() {
7028                    return v.metric_key;
7029                }
7030            }
7031        }
7032
7033        // Helper: drive a single `check_flood` counter past its threshold.
7034        fn key_from_check_flood(setup: impl FnOnce(&mut H2FloodDetector)) -> &'static str {
7035            let mut detector = H2FloodDetector::default();
7036            setup(&mut detector);
7037            detector
7038                .check_flood()
7039                .expect("setup should always trip a flood")
7040                .metric_key
7041        }
7042
7043        let keys: [&'static str; 12] = [
7044            // Lifetime methods on the detector itself.
7045            key_from_rst_lifetime(true),
7046            key_from_rst_lifetime(false),
7047            key_from_rst_emitted(),
7048            // `check_flood` arms.
7049            key_from_check_flood(|d| d.rst_stream_count = u32::MAX),
7050            key_from_check_flood(|d| d.ping_count = u32::MAX),
7051            key_from_check_flood(|d| d.total_ping_received_lifetime = u32::MAX),
7052            key_from_check_flood(|d| d.settings_count = u32::MAX),
7053            key_from_check_flood(|d| d.total_settings_received_lifetime = u32::MAX),
7054            key_from_check_flood(|d| d.empty_data_count = u32::MAX),
7055            key_from_check_flood(|d| d.continuation_count = u32::MAX),
7056            key_from_check_flood(|d| d.accumulated_header_size = u32::MAX),
7057            key_from_check_flood(|d| d.glitch_count = u32::MAX),
7058        ];
7059
7060        for key in keys {
7061            assert!(
7062                key.starts_with("h2.flood.violation."),
7063                "metric key {key} is missing the h2.flood.violation. prefix",
7064            );
7065        }
7066        let mut deduped = keys.to_vec();
7067        deduped.sort_unstable();
7068        deduped.dedup();
7069        assert_eq!(
7070            deduped.len(),
7071            keys.len(),
7072            "metric keys must be unique across violation kinds; collisions: {keys:?}",
7073        );
7074    }
7075
7076    /// All four `metric_for_*` helpers must yield distinct, namespaced keys for
7077    /// every RFC 9113 §7 error code. The macro behind them uses `concat!`, so a
7078    /// new H2Error variant fails the build inside the macro — but a typo in
7079    /// the helper prefix would silently land. Walk every (direction × kind)
7080    /// pair and dedupe the set.
7081    /// `h2_frame_rx_metric_key` must yield a distinct `&'static str` per
7082    /// `Frame::*` variant. The single dispatch site in `handle_frame` reads
7083    /// from this helper, so a typo or duplicate would silently clobber the
7084    /// frame-mix dashboard. Asserting the literal set lets us compare against
7085    /// `doc/configure.md` and the RFC 9113 §6 frame catalogue without
7086    /// reconstructing every Frame variant in the test.
7087    #[test]
7088    fn test_h2_frame_rx_metric_keys_are_unique_and_namespaced() {
7089        // Update this list whenever a new Frame variant is added — the helper
7090        // match is also exhaustive, so the build will already break there
7091        // before anyone notices the test missing a key.
7092        let expected: [&'static str; 11] = [
7093            "h2.frames.rx.data",
7094            "h2.frames.rx.headers",
7095            "h2.frames.rx.push_promise",
7096            "h2.frames.rx.priority",
7097            "h2.frames.rx.rst_stream",
7098            "h2.frames.rx.settings",
7099            "h2.frames.rx.ping",
7100            "h2.frames.rx.goaway",
7101            "h2.frames.rx.window_update",
7102            "h2.frames.rx.continuation",
7103            "h2.frames.rx.unknown",
7104        ];
7105
7106        for key in expected {
7107            assert!(
7108                key.starts_with("h2.frames.rx."),
7109                "metric key {key} is missing the h2.frames.rx. prefix",
7110            );
7111        }
7112        let mut deduped = expected.to_vec();
7113        deduped.sort_unstable();
7114        deduped.dedup();
7115        assert_eq!(
7116            deduped.len(),
7117            expected.len(),
7118            "frame-rx metric keys must be unique; collisions in: {expected:?}",
7119        );
7120
7121        // Spot-check the helper for the one variant we can construct without
7122        // borrowing into a frame body — `Frame::Unknown(u8)` is just a tag.
7123        assert_eq!(
7124            h2_frame_rx_metric_key(&Frame::Unknown(42)),
7125            "h2.frames.rx.unknown",
7126        );
7127    }
7128
7129    #[test]
7130    fn test_per_error_code_metric_keys_are_unique_and_namespaced() {
7131        const ALL_ERRORS: [H2Error; 14] = [
7132            H2Error::NoError,
7133            H2Error::ProtocolError,
7134            H2Error::InternalError,
7135            H2Error::FlowControlError,
7136            H2Error::SettingsTimeout,
7137            H2Error::StreamClosed,
7138            H2Error::FrameSizeError,
7139            H2Error::RefusedStream,
7140            H2Error::Cancel,
7141            H2Error::CompressionError,
7142            H2Error::ConnectError,
7143            H2Error::EnhanceYourCalm,
7144            H2Error::InadequateSecurity,
7145            H2Error::HTTP11Required,
7146        ];
7147
7148        let mut keys: Vec<&'static str> = Vec::new();
7149        for error in ALL_ERRORS {
7150            let code = error as u32;
7151            keys.push(metric_for_goaway_sent(error));
7152            keys.push(metric_for_goaway_received(code));
7153            keys.push(metric_for_rst_stream_sent(error));
7154            keys.push(metric_for_rst_stream_received(code));
7155        }
7156        // …plus the four `unknown_error` fallbacks for codes outside RFC 9113 §7.
7157        let unknown_code = 0xff;
7158        assert!(H2Error::try_from(unknown_code).is_err());
7159        keys.push(metric_for_goaway_received(unknown_code));
7160        keys.push(metric_for_rst_stream_received(unknown_code));
7161        // …and the dedicated Rapid Reset signature counter.
7162        keys.push(names::h2::RST_STREAM_RECEIVED_PRE_RESPONSE_START);
7163
7164        for key in &keys {
7165            assert!(
7166                key.starts_with("h2.goaway.sent.")
7167                    || key.starts_with("h2.goaway.received.")
7168                    || key.starts_with("h2.rst_stream.sent.")
7169                    || key.starts_with("h2.rst_stream.received."),
7170                "metric key {key} does not match a known per-error-code namespace",
7171            );
7172        }
7173        let mut deduped = keys.clone();
7174        deduped.sort_unstable();
7175        deduped.dedup();
7176        assert_eq!(
7177            deduped.len(),
7178            keys.len(),
7179            "per-error-code metric keys must be unique; collisions in: {keys:?}",
7180        );
7181    }
7182
7183    #[test]
7184    fn test_flood_detector_response_started_rst_not_abusive() {
7185        // When the backend response has begun, the RST is cheap for us
7186        // too — it only bumps the generic lifetime counter.
7187        let mut detector = H2FloodDetector::default();
7188        for _ in 0..(DEFAULT_MAX_RST_STREAM_ABUSIVE_LIFETIME + 100) {
7189            assert!(detector.record_rst_lifetime(true).is_none());
7190        }
7191        assert_eq!(detector.total_abusive_rst_received_lifetime, 0);
7192        assert_eq!(
7193            detector.total_rst_received_lifetime,
7194            DEFAULT_MAX_RST_STREAM_ABUSIVE_LIFETIME + 100
7195        );
7196    }
7197
7198    #[test]
7199    fn test_flood_detector_default_matches_new_default() {
7200        let from_default = H2FloodDetector::default();
7201        let from_new = H2FloodDetector::new(H2FloodConfig::default());
7202
7203        assert_eq!(from_default.rst_stream_count, from_new.rst_stream_count);
7204        assert_eq!(from_default.ping_count, from_new.ping_count);
7205        assert_eq!(from_default.settings_count, from_new.settings_count);
7206        assert_eq!(from_default.empty_data_count, from_new.empty_data_count);
7207        assert_eq!(from_default.continuation_count, from_new.continuation_count);
7208        assert_eq!(
7209            from_default.accumulated_header_size,
7210            from_new.accumulated_header_size
7211        );
7212        assert_eq!(from_default.glitch_count, from_new.glitch_count);
7213        assert_eq!(from_default.config, from_new.config);
7214    }
7215
7216    // ── Prioriser ────────────────────────────────────────────────────────
7217
7218    #[test]
7219    fn test_prioriser_defaults_for_unknown_stream() {
7220        let p = Prioriser::default();
7221        // Unknown stream -> RFC 9218 defaults: urgency 3, incremental false
7222        assert_eq!(p.get(&1), (3, false));
7223        assert_eq!(p.get(&999), (3, false));
7224    }
7225
7226    #[test]
7227    fn test_prioriser_push_rfc9218_and_get() {
7228        let mut p = Prioriser::default();
7229
7230        let invalid = p.push_priority(
7231            1,
7232            parser::PriorityPart::Rfc9218 {
7233                urgency: 0,
7234                incremental: true,
7235            },
7236        );
7237        assert!(!invalid);
7238        assert_eq!(p.get(&1), (0, true));
7239
7240        let invalid = p.push_priority(
7241            3,
7242            parser::PriorityPart::Rfc9218 {
7243                urgency: 7,
7244                incremental: false,
7245            },
7246        );
7247        assert!(!invalid);
7248        assert_eq!(p.get(&3), (7, false));
7249    }
7250
7251    #[test]
7252    fn test_prioriser_urgency_clamped_to_7() {
7253        let mut p = Prioriser::default();
7254
7255        p.push_priority(
7256            1,
7257            parser::PriorityPart::Rfc9218 {
7258                urgency: 255,
7259                incremental: false,
7260            },
7261        );
7262        assert_eq!(p.get(&1), (7, false));
7263    }
7264
7265    #[test]
7266    fn test_prioriser_update_priority() {
7267        let mut p = Prioriser::default();
7268
7269        p.push_priority(
7270            1,
7271            parser::PriorityPart::Rfc9218 {
7272                urgency: 3,
7273                incremental: false,
7274            },
7275        );
7276        assert_eq!(p.get(&1), (3, false));
7277
7278        // Update same stream
7279        p.push_priority(
7280            1,
7281            parser::PriorityPart::Rfc9218 {
7282                urgency: 1,
7283                incremental: true,
7284            },
7285        );
7286        assert_eq!(p.get(&1), (1, true));
7287    }
7288
7289    #[test]
7290    fn test_prioriser_remove() {
7291        let mut p = Prioriser::default();
7292
7293        p.push_priority(
7294            1,
7295            parser::PriorityPart::Rfc9218 {
7296                urgency: 0,
7297                incremental: true,
7298            },
7299        );
7300        assert_eq!(p.get(&1), (0, true));
7301
7302        p.remove(&1);
7303        // After removal, falls back to defaults
7304        assert_eq!(p.get(&1), (3, false));
7305    }
7306
7307    #[test]
7308    fn test_prioriser_rfc7540_self_dependency() {
7309        let mut p = Prioriser::default();
7310
7311        // Self-dependency should return true (invalid)
7312        let invalid = p.push_priority(
7313            5,
7314            parser::PriorityPart::Rfc7540 {
7315                stream_dependency: parser::StreamDependency {
7316                    exclusive: false,
7317                    stream_id: 5, // same as stream_id
7318                },
7319                weight: 16,
7320            },
7321        );
7322        assert!(invalid);
7323    }
7324
7325    #[test]
7326    fn test_prioriser_rfc7540_valid_dependency() {
7327        let mut p = Prioriser::default();
7328
7329        // Non-self dependency is valid (but ignored for scheduling)
7330        let invalid = p.push_priority(
7331            5,
7332            parser::PriorityPart::Rfc7540 {
7333                stream_dependency: parser::StreamDependency {
7334                    exclusive: false,
7335                    stream_id: 3, // different stream
7336                },
7337                weight: 16,
7338            },
7339        );
7340        assert!(!invalid);
7341        // Still returns defaults since RFC 7540 priority is ignored
7342        assert_eq!(p.get(&5), (3, false));
7343    }
7344
7345    #[test]
7346    fn test_prioriser_max_entries_cap() {
7347        let mut p = Prioriser::default();
7348
7349        // Fill up to MAX_PRIORITIES
7350        for i in 0..MAX_PRIORITIES as u32 {
7351            let stream_id = i * 2 + 1; // odd stream IDs
7352            p.push_priority(
7353                stream_id,
7354                parser::PriorityPart::Rfc9218 {
7355                    urgency: (i % 8) as u8,
7356                    incremental: false,
7357                },
7358            );
7359        }
7360
7361        // Next insert for a new stream should be silently rejected
7362        let next_id = (MAX_PRIORITIES as u32) * 2 + 1;
7363        let invalid = p.push_priority(
7364            next_id,
7365            parser::PriorityPart::Rfc9218 {
7366                urgency: 0,
7367                incremental: true,
7368            },
7369        );
7370        assert!(!invalid); // not a protocol error, just silently dropped
7371        assert_eq!(p.get(&next_id), (3, false)); // defaults, not stored
7372    }
7373
7374    #[test]
7375    fn test_prioriser_update_existing_at_cap() {
7376        let mut p = Prioriser::default();
7377
7378        // Fill to cap
7379        for i in 0..MAX_PRIORITIES as u32 {
7380            p.push_priority(
7381                i * 2 + 1,
7382                parser::PriorityPart::Rfc9218 {
7383                    urgency: 3,
7384                    incremental: false,
7385                },
7386            );
7387        }
7388
7389        // Updating an existing entry should still work even at cap
7390        p.push_priority(
7391            1,
7392            parser::PriorityPart::Rfc9218 {
7393                urgency: 0,
7394                incremental: true,
7395            },
7396        );
7397        assert_eq!(p.get(&1), (0, true));
7398    }
7399
7400    #[test]
7401    fn test_prioriser_guarded_accepts_open_stream() {
7402        let mut p = Prioriser::default();
7403        let mut open: HashMap<StreamId, GlobalStreamId> = HashMap::new();
7404        open.insert(3, 0);
7405        let invalid = p.push_priority_guarded(
7406            3,
7407            parser::PriorityPart::Rfc9218 {
7408                urgency: 1,
7409                incremental: false,
7410            },
7411            7,
7412            &open,
7413        );
7414        assert!(!invalid);
7415        assert_eq!(p.get(&3), (1, false));
7416    }
7417
7418    #[test]
7419    fn test_prioriser_guarded_accepts_idle_lookahead() {
7420        let mut p = Prioriser::default();
7421        let open: HashMap<StreamId, GlobalStreamId> = HashMap::new();
7422        // Just ahead of last_stream_id, within PRIORITY_IDLE_LOOKAHEAD.
7423        let invalid = p.push_priority_guarded(
7424            105,
7425            parser::PriorityPart::Rfc9218 {
7426                urgency: 2,
7427                incremental: true,
7428            },
7429            99,
7430            &open,
7431        );
7432        assert!(!invalid);
7433        assert_eq!(p.get(&105), (2, true));
7434    }
7435
7436    #[test]
7437    fn test_prioriser_guarded_drops_far_future_stream() {
7438        let mut p = Prioriser::default();
7439        let open: HashMap<StreamId, GlobalStreamId> = HashMap::new();
7440        // Beyond the 64-slot lookahead window.
7441        let invalid = p.push_priority_guarded(
7442            1_000_001,
7443            parser::PriorityPart::Rfc9218 {
7444                urgency: 0,
7445                incremental: false,
7446            },
7447            3,
7448            &open,
7449        );
7450        assert!(!invalid); // not a protocol error, just dropped
7451        // Default priority returned — no entry stored.
7452        assert_eq!(p.get(&1_000_001), (DEFAULT_URGENCY, false));
7453    }
7454
7455    #[test]
7456    fn test_prioriser_guarded_drops_closed_past_stream() {
7457        let mut p = Prioriser::default();
7458        let open: HashMap<StreamId, GlobalStreamId> = HashMap::new();
7459        // Past the counter and not open = already closed. Drop.
7460        let invalid = p.push_priority_guarded(
7461            3,
7462            parser::PriorityPart::Rfc9218 {
7463                urgency: 5,
7464                incremental: false,
7465            },
7466            99,
7467            &open,
7468        );
7469        assert!(!invalid);
7470        assert_eq!(p.get(&3), (DEFAULT_URGENCY, false));
7471    }
7472
7473    #[test]
7474    fn test_prioriser_guarded_cannot_flood_with_far_ids() {
7475        // Previously an attacker could pack MAX_PRIORITIES entries by picking
7476        // far-future stream IDs. The guard rejects them before the cap helps.
7477        let mut p = Prioriser::default();
7478        let open: HashMap<StreamId, GlobalStreamId> = HashMap::new();
7479        for delta in 10_000..(10_000 + MAX_PRIORITIES as u32) {
7480            p.push_priority_guarded(
7481                delta,
7482                parser::PriorityPart::Rfc9218 {
7483                    urgency: 0,
7484                    incremental: false,
7485                },
7486                0,
7487                &open,
7488            );
7489        }
7490        assert_eq!(p.priorities.len(), 0);
7491    }
7492
7493    // ── RFC 9218 §4 round-robin rotation ───────────────────────────────
7494
7495    /// Helper: mark `stream_id` as (urgency, incremental) in the map.
7496    fn set_prio(p: &mut Prioriser, stream_id: StreamId, urgency: u8, incremental: bool) {
7497        p.push_priority(
7498            stream_id,
7499            parser::PriorityPart::Rfc9218 {
7500                urgency,
7501                incremental,
7502            },
7503        );
7504    }
7505
7506    #[test]
7507    fn test_apply_incremental_rotation_all_non_incremental_is_noop() {
7508        // Non-incremental streams keep the existing (urgency, stream_id) sort.
7509        let mut p = Prioriser::default();
7510        set_prio(&mut p, 1, 3, false);
7511        set_prio(&mut p, 3, 3, false);
7512        set_prio(&mut p, 5, 3, false);
7513
7514        let mut buf = vec![1u32, 3, 5];
7515        let count = p.apply_incremental_rotation(&mut buf);
7516        assert_eq!(count, 0);
7517        assert_eq!(buf, vec![1, 3, 5]);
7518    }
7519
7520    #[test]
7521    fn test_apply_incremental_rotation_moves_incremental_to_tail() {
7522        // Within a same-urgency bucket non-incremental must come before
7523        // incremental, each subrange staying ascending.
7524        let mut p = Prioriser::default();
7525        set_prio(&mut p, 1, 3, true);
7526        set_prio(&mut p, 3, 3, false);
7527        set_prio(&mut p, 5, 3, true);
7528        set_prio(&mut p, 7, 3, false);
7529
7530        let mut buf = vec![1u32, 3, 5, 7];
7531        let count = p.apply_incremental_rotation(&mut buf);
7532        assert_eq!(count, 2);
7533        // Non-incremental first (3, 7), then incremental (1, 5) — ascending
7534        // within each subrange before the cursor rotation.
7535        assert_eq!(buf, vec![3, 7, 1, 5]);
7536    }
7537
7538    #[test]
7539    fn test_apply_incremental_rotation_respects_urgency_buckets() {
7540        // Different urgency buckets must not be mixed.
7541        let mut p = Prioriser::default();
7542        set_prio(&mut p, 1, 0, true); // urgent incremental
7543        set_prio(&mut p, 3, 3, false); // default non-incremental
7544        set_prio(&mut p, 5, 3, true); // default incremental
7545        set_prio(&mut p, 7, 5, false); // low-priority non-incremental
7546
7547        // Input is pre-sorted by (urgency, id) as the scheduler does.
7548        let mut buf = vec![1u32, 3, 5, 7];
7549        let count = p.apply_incremental_rotation(&mut buf);
7550        assert_eq!(count, 2);
7551        // Bucket 0: [1] (alone, stays). Bucket 3: [3] non-inc, [5] inc.
7552        // Bucket 5: [7] alone. Cross-bucket order is preserved.
7553        assert_eq!(buf, vec![1, 3, 5, 7]);
7554    }
7555
7556    #[test]
7557    fn test_apply_incremental_rotation_rotates_by_cursor() {
7558        // Three same-urgency incremental streams: cursor advancement shifts
7559        // the bucket so the next pass starts after the previously fired ID.
7560        let mut p = Prioriser::default();
7561        set_prio(&mut p, 1, 3, true);
7562        set_prio(&mut p, 3, 3, true);
7563        set_prio(&mut p, 5, 3, true);
7564
7565        let base = vec![1u32, 3, 5];
7566
7567        // Pass 1: cursor is 0 (initial), so order stays 1, 3, 5.
7568        let mut buf = base.clone();
7569        assert_eq!(p.apply_incremental_rotation(&mut buf), 3);
7570        assert_eq!(buf, vec![1, 3, 5]);
7571        p.advance_incremental_cursor(Some(1));
7572
7573        // Pass 2: cursor is 1, rotate so 3 comes first.
7574        let mut buf = base.clone();
7575        assert_eq!(p.apply_incremental_rotation(&mut buf), 3);
7576        assert_eq!(buf, vec![3, 5, 1]);
7577        p.advance_incremental_cursor(Some(3));
7578
7579        // Pass 3: cursor is 3, rotate so 5 comes first.
7580        let mut buf = base.clone();
7581        assert_eq!(p.apply_incremental_rotation(&mut buf), 3);
7582        assert_eq!(buf, vec![5, 1, 3]);
7583        p.advance_incremental_cursor(Some(5));
7584
7585        // Pass 4: cursor is 5 (largest in bucket), wrap to 1.
7586        let mut buf = base;
7587        assert_eq!(p.apply_incremental_rotation(&mut buf), 3);
7588        assert_eq!(buf, vec![1, 3, 5]);
7589    }
7590
7591    #[test]
7592    fn test_apply_incremental_rotation_cursor_unknown_id() {
7593        // Cursor points at an ID no longer active (stream completed). Rotation
7594        // should still start from the smallest ID greater than the cursor.
7595        let mut p = Prioriser::default();
7596        set_prio(&mut p, 3, 3, true);
7597        set_prio(&mut p, 5, 3, true);
7598        set_prio(&mut p, 7, 3, true);
7599        p.advance_incremental_cursor(Some(4)); // 4 is not in the bucket
7600
7601        let mut buf = vec![3u32, 5, 7];
7602        assert_eq!(p.apply_incremental_rotation(&mut buf), 3);
7603        assert_eq!(buf, vec![5, 7, 3]);
7604    }
7605
7606    #[test]
7607    fn test_apply_incremental_rotation_single_stream_buckets() {
7608        // Single-stream buckets are a degenerate fast path: no reordering.
7609        let mut p = Prioriser::default();
7610        set_prio(&mut p, 1, 1, true);
7611        set_prio(&mut p, 3, 2, false);
7612        set_prio(&mut p, 5, 3, true);
7613
7614        let mut buf = vec![1u32, 3, 5];
7615        let count = p.apply_incremental_rotation(&mut buf);
7616        assert_eq!(count, 2);
7617        assert_eq!(buf, vec![1, 3, 5]);
7618    }
7619
7620    #[test]
7621    fn test_advance_incremental_cursor_none_is_noop() {
7622        // If no incremental stream fires (only non-incremental served), the
7623        // cursor must stay put so fairness is preserved for the next pass.
7624        let mut p = Prioriser::default();
7625        p.advance_incremental_cursor(Some(5));
7626        p.advance_incremental_cursor(None);
7627        assert_eq!(p.incremental_cursor, 5);
7628    }
7629
7630    #[test]
7631    fn test_apply_incremental_rotation_mixed_bucket_with_cursor() {
7632        // Same-urgency bucket with a mix: non-inc served first in ascending
7633        // order, then the incremental tail rotated by cursor.
7634        let mut p = Prioriser::default();
7635        set_prio(&mut p, 1, 3, true);
7636        set_prio(&mut p, 3, 3, false);
7637        set_prio(&mut p, 5, 3, true);
7638        set_prio(&mut p, 7, 3, false);
7639        set_prio(&mut p, 9, 3, true);
7640        p.advance_incremental_cursor(Some(5));
7641
7642        let mut buf = vec![1u32, 3, 5, 7, 9];
7643        let count = p.apply_incremental_rotation(&mut buf);
7644        assert_eq!(count, 3);
7645        // Non-inc (3, 7) first, then incremental rotated: cursor 5 means
7646        // next-after-5 = 9, then 1, then 5 (wrap).
7647        assert_eq!(buf, vec![3, 7, 9, 1, 5]);
7648    }
7649
7650    // ── H2FlowControl ───────────────────────────────────────────────────
7651
7652    #[test]
7653    fn test_flow_control_initial_state() {
7654        let fc = H2FlowControl {
7655            window: DEFAULT_INITIAL_WINDOW_SIZE as i32,
7656            received_bytes_since_update: 0,
7657            pending_window_updates: HashMap::new(),
7658        };
7659        assert_eq!(fc.window, 65535);
7660        assert_eq!(fc.received_bytes_since_update, 0);
7661        assert!(fc.pending_window_updates.is_empty());
7662    }
7663
7664    #[test]
7665    fn test_flow_control_window_update_coalescing() {
7666        let mut updates: HashMap<u32, u32> = HashMap::new();
7667
7668        // First update for stream 1
7669        updates.insert(1, 1000);
7670        assert_eq!(*updates.get(&1).unwrap(), 1000);
7671
7672        // Coalesce second update for same stream
7673        if let Some(existing) = updates.get_mut(&1) {
7674            *existing = existing.saturating_add(500).min(i32::MAX as u32);
7675        }
7676        assert_eq!(*updates.get(&1).unwrap(), 1500);
7677
7678        // Different stream gets its own entry
7679        updates.insert(3, 2000);
7680        assert_eq!(updates.len(), 2);
7681        assert_eq!(*updates.get(&3).unwrap(), 2000);
7682    }
7683
7684    #[test]
7685    fn test_flow_control_window_update_saturation() {
7686        let mut updates: HashMap<u32, u32> = HashMap::new();
7687
7688        // Insert near max and coalesce — should saturate to i32::MAX
7689        let max_increment = i32::MAX as u32;
7690        updates.insert(1, max_increment - 100);
7691        if let Some(existing) = updates.get_mut(&1) {
7692            *existing = existing.saturating_add(200).min(max_increment);
7693        }
7694        assert_eq!(*updates.get(&1).unwrap(), max_increment);
7695    }
7696
7697    #[test]
7698    fn test_flow_control_connection_window_can_go_negative() {
7699        // RFC 9113 §6.9.2: connection-level window can go negative
7700        let mut fc = H2FlowControl {
7701            window: 100,
7702            received_bytes_since_update: 0,
7703            pending_window_updates: HashMap::new(),
7704        };
7705
7706        // Simulate consuming more than available
7707        fc.window -= 200;
7708        assert_eq!(fc.window, -100);
7709    }
7710
7711    // ── H2FloodConfig ───────────────────────────────────────────────────
7712
7713    #[test]
7714    fn test_flood_config_default_values() {
7715        let config = H2FloodConfig::default();
7716        assert_eq!(config.max_rst_stream_per_window, 100);
7717        assert_eq!(config.max_ping_per_window, 100);
7718        assert_eq!(config.max_settings_per_window, 50);
7719        assert_eq!(config.max_empty_data_per_window, 100);
7720        assert_eq!(config.max_continuation_frames, 20);
7721        assert_eq!(config.max_glitch_count, 100);
7722        assert_eq!(config.max_rst_stream_lifetime, 10_000);
7723        assert_eq!(config.max_rst_stream_abusive_lifetime, 50);
7724        assert_eq!(config.max_header_list_size, MAX_HEADER_LIST_SIZE as u32);
7725    }
7726
7727    // ── distribute_overhead ─────────────────────────────────────────────
7728
7729    #[test]
7730    fn test_distribute_overhead_proportional() {
7731        let mut metrics = SessionMetrics::new(None);
7732        let mut overhead_bin = 1000;
7733        let mut overhead_bout = 500;
7734
7735        // Stream transferred 60% of total bytes (not last stream)
7736        distribute_overhead(
7737            &mut metrics,
7738            &mut overhead_bin,
7739            &mut overhead_bout,
7740            (600, 300),  // stream_bytes
7741            (1000, 500), // total_bytes
7742            2,           // active_streams
7743            false,       // is_last_stream
7744        );
7745
7746        assert_eq!(metrics.bin, 600); // 60% of 1000
7747        assert_eq!(metrics.bout, 300); // 60% of 500
7748        assert_eq!(overhead_bin, 400); // 1000 - 600
7749        assert_eq!(overhead_bout, 200); // 500 - 300
7750    }
7751
7752    #[test]
7753    fn test_distribute_overhead_even_split_when_no_bytes() {
7754        let mut metrics = SessionMetrics::new(None);
7755        let mut overhead_bin = 100;
7756        let mut overhead_bout = 200;
7757
7758        // No bytes transferred -> even distribution (not last stream)
7759        distribute_overhead(
7760            &mut metrics,
7761            &mut overhead_bin,
7762            &mut overhead_bout,
7763            (0, 0), // stream_bytes
7764            (0, 0), // total_bytes
7765            4,      // active_streams
7766            false,  // is_last_stream
7767        );
7768
7769        assert_eq!(metrics.bin, 25); // 100 / 4
7770        assert_eq!(metrics.bout, 50); // 200 / 4
7771        assert_eq!(overhead_bin, 75);
7772        assert_eq!(overhead_bout, 150);
7773    }
7774
7775    #[test]
7776    fn test_distribute_overhead_clamps_to_remaining() {
7777        let mut metrics = SessionMetrics::new(None);
7778        let mut overhead_bin = 10;
7779        let mut overhead_bout = 10;
7780
7781        // Stream claims 100% of bytes but overhead is small (last stream)
7782        distribute_overhead(
7783            &mut metrics,
7784            &mut overhead_bin,
7785            &mut overhead_bout,
7786            (1000, 1000), // stream_bytes
7787            (1000, 1000), // total_bytes
7788            1,            // active_streams
7789            true,         // is_last_stream
7790        );
7791
7792        assert_eq!(metrics.bin, 10);
7793        assert_eq!(metrics.bout, 10);
7794        assert_eq!(overhead_bin, 0);
7795        assert_eq!(overhead_bout, 0);
7796    }
7797
7798    #[test]
7799    fn test_distribute_overhead_zero_active_streams() {
7800        let mut metrics = SessionMetrics::new(None);
7801        let mut overhead_bin = 100;
7802        let mut overhead_bout = 100;
7803
7804        // 0 active streams (edge case) — last stream gets all remainder
7805        distribute_overhead(
7806            &mut metrics,
7807            &mut overhead_bin,
7808            &mut overhead_bout,
7809            (0, 0),
7810            (0, 0),
7811            0,
7812            true,
7813        );
7814
7815        assert_eq!(metrics.bin, 100); // last stream gets all remaining
7816        assert_eq!(metrics.bout, 100);
7817        assert_eq!(overhead_bin, 0);
7818        assert_eq!(overhead_bout, 0);
7819    }
7820
7821    #[test]
7822    fn test_distribute_overhead_last_stream_gets_remainder() {
7823        let mut metrics1 = SessionMetrics::new(None);
7824        let mut metrics2 = SessionMetrics::new(None);
7825        let mut overhead_bin = 120;
7826        let mut overhead_bout = 120;
7827
7828        // First stream (not last): gets proportional share
7829        distribute_overhead(
7830            &mut metrics1,
7831            &mut overhead_bin,
7832            &mut overhead_bout,
7833            (100, 100), // stream_bytes
7834            (300, 300), // total_bytes
7835            3,          // active_streams
7836            false,      // is_last_stream
7837        );
7838
7839        let remaining_bin = overhead_bin;
7840        let remaining_bout = overhead_bout;
7841
7842        // Last stream: gets ALL remaining overhead (no rounding loss)
7843        distribute_overhead(
7844            &mut metrics2,
7845            &mut overhead_bin,
7846            &mut overhead_bout,
7847            (100, 100), // stream_bytes
7848            (300, 300), // total_bytes
7849            3,          // active_streams
7850            true,       // is_last_stream
7851        );
7852
7853        assert_eq!(metrics2.bin, remaining_bin);
7854        assert_eq!(metrics2.bout, remaining_bout);
7855        assert_eq!(overhead_bin, 0, "no remainder bytes should be lost");
7856        assert_eq!(overhead_bout, 0, "no remainder bytes should be lost");
7857    }
7858
7859    // ── H2FlowControl (additional edge cases) ─────────────────────────
7860
7861    #[test]
7862    fn test_flow_control_queue_window_update_cap() {
7863        // Verify DEFAULT_MAX_PENDING_WINDOW_UPDATES reflects 1 + 4*MAX_CONCURRENT_STREAMS
7864        assert_eq!(DEFAULT_MAX_PENDING_WINDOW_UPDATES, 1 + 100 * 4);
7865
7866        // Simulate queue reaching capacity
7867        let cap = DEFAULT_MAX_PENDING_WINDOW_UPDATES;
7868        let mut updates: HashMap<u32, u32> = HashMap::new();
7869        for i in 0..cap as u32 {
7870            updates.insert(i, 1000);
7871        }
7872        assert_eq!(updates.len(), cap);
7873
7874        // A new stream ID beyond capacity should be rejected
7875        let next_stream = cap as u32;
7876        let at_cap = updates.len() >= cap;
7877        assert!(at_cap);
7878        assert!(!updates.contains_key(&next_stream));
7879
7880        // Verify custom max_concurrent_streams produces proportional cap
7881        let custom_cap = 1 + 500_usize * 4;
7882        assert_eq!(custom_cap, 2001);
7883    }
7884
7885    #[test]
7886    fn test_h2_connection_config_defaults() {
7887        let config = H2ConnectionConfig::default();
7888        assert_eq!(config.initial_connection_window, ENLARGED_CONNECTION_WINDOW);
7889        assert_eq!(
7890            config.max_concurrent_streams,
7891            DEFAULT_MAX_CONCURRENT_STREAMS
7892        );
7893        assert_eq!(config.stream_shrink_ratio, 2);
7894    }
7895
7896    #[test]
7897    fn test_h2_connection_config_clamp_window_lower_bound() {
7898        // Below minimum: clamped to DEFAULT_INITIAL_WINDOW_SIZE (65535)
7899        let config = H2ConnectionConfig::new(100, 100, 2);
7900        assert_eq!(
7901            config.initial_connection_window,
7902            DEFAULT_INITIAL_WINDOW_SIZE
7903        );
7904    }
7905
7906    #[test]
7907    fn test_h2_connection_config_clamp_window_upper_bound() {
7908        // Above maximum: clamped to FLOW_CONTROL_MAX_WINDOW (2^31-1)
7909        let config = H2ConnectionConfig::new(u32::MAX, 100, 2);
7910        assert_eq!(config.initial_connection_window, FLOW_CONTROL_MAX_WINDOW);
7911    }
7912
7913    #[test]
7914    fn test_h2_connection_config_clamp_window_exact_minimum() {
7915        // Exactly minimum: no clamping, no zero-increment WINDOW_UPDATE risk
7916        let config = H2ConnectionConfig::new(DEFAULT_INITIAL_WINDOW_SIZE, 100, 2);
7917        assert_eq!(
7918            config.initial_connection_window,
7919            DEFAULT_INITIAL_WINDOW_SIZE
7920        );
7921        // Increment to send would be 0 — the code guards this with `if increment > 0`
7922        let increment = config
7923            .initial_connection_window
7924            .saturating_sub(DEFAULT_INITIAL_WINDOW_SIZE);
7925        assert_eq!(increment, 0);
7926    }
7927
7928    #[test]
7929    fn test_h2_connection_config_clamp_shrink_ratio() {
7930        // Below minimum: clamped to 2 (1 would defeat recycling)
7931        let config = H2ConnectionConfig::new(ENLARGED_CONNECTION_WINDOW, 100, 0);
7932        assert_eq!(config.stream_shrink_ratio, 2);
7933        let config = H2ConnectionConfig::new(ENLARGED_CONNECTION_WINDOW, 100, 1);
7934        assert_eq!(config.stream_shrink_ratio, 2);
7935    }
7936
7937    #[test]
7938    fn test_h2_connection_config_clamp_concurrent_streams() {
7939        let config = H2ConnectionConfig::new(ENLARGED_CONNECTION_WINDOW, 0, 2);
7940        assert_eq!(config.max_concurrent_streams, 1);
7941    }
7942
7943    #[test]
7944    fn test_h2_connection_config_from_optional_uses_defaults() {
7945        let config = H2ConnectionConfig::from_optional(None, None, None);
7946        let defaults = H2ConnectionConfig::default();
7947        assert_eq!(config, defaults);
7948    }
7949
7950    #[test]
7951    fn test_h2_connection_config_from_optional_overrides() {
7952        let config = H2ConnectionConfig::from_optional(Some(2_000_000), Some(500), Some(4));
7953        assert_eq!(config.initial_connection_window, 2_000_000);
7954        assert_eq!(config.max_concurrent_streams, 500);
7955        assert_eq!(config.stream_shrink_ratio, 4);
7956    }
7957
7958    #[test]
7959    fn test_flow_control_window_settings_change_negative() {
7960        // RFC 9113 §6.9.2: A change to SETTINGS_INITIAL_WINDOW_SIZE can cause
7961        // the flow-control window to become negative.
7962        let mut fc = H2FlowControl {
7963            window: 100,
7964            received_bytes_since_update: 0,
7965            pending_window_updates: HashMap::new(),
7966        };
7967
7968        // Simulate SETTINGS_INITIAL_WINDOW_SIZE reduction:
7969        // old_initial = 65535, new_initial = 10 => delta = 10 - 65535 = -65525
7970        let old_initial: i32 = DEFAULT_INITIAL_WINDOW_SIZE as i32;
7971        let new_initial: i32 = 10;
7972        let delta = new_initial - old_initial; // -65525
7973        fc.window += delta;
7974
7975        assert!(
7976            fc.window < 0,
7977            "Window must be able to go negative after settings change"
7978        );
7979        assert_eq!(fc.window, 100 + (10 - 65535));
7980    }
7981
7982    #[test]
7983    fn test_flow_control_coalesce_saturates_at_max_increment() {
7984        let max_increment = i32::MAX as u32;
7985        let mut updates: HashMap<u32, u32> = HashMap::new();
7986
7987        // Insert at max and try to coalesce more
7988        updates.insert(1, max_increment);
7989        if let Some(existing) = updates.get_mut(&1) {
7990            *existing = existing.saturating_add(1000).min(max_increment);
7991        }
7992        assert_eq!(*updates.get(&1).unwrap(), max_increment);
7993    }
7994
7995    // ── H2FloodConfig (additional) ───────────────────────────────────
7996
7997    #[test]
7998    fn test_flood_config_default_matches_constants() {
7999        let config = H2FloodConfig::default();
8000        assert_eq!(
8001            config.max_rst_stream_per_window,
8002            DEFAULT_MAX_RST_STREAM_PER_WINDOW
8003        );
8004        assert_eq!(config.max_ping_per_window, DEFAULT_MAX_PING_PER_WINDOW);
8005        assert_eq!(
8006            config.max_settings_per_window,
8007            DEFAULT_MAX_SETTINGS_PER_WINDOW
8008        );
8009        assert_eq!(
8010            config.max_empty_data_per_window,
8011            DEFAULT_MAX_EMPTY_DATA_PER_WINDOW
8012        );
8013        assert_eq!(
8014            config.max_continuation_frames,
8015            DEFAULT_MAX_CONTINUATION_FRAMES
8016        );
8017        assert_eq!(config.max_glitch_count, DEFAULT_MAX_GLITCH_COUNT);
8018    }
8019
8020    #[test]
8021    fn test_flood_config_equality() {
8022        let config_a = H2FloodConfig::default();
8023        let config_b = H2FloodConfig::default();
8024        assert_eq!(config_a, config_b);
8025
8026        let config_c = H2FloodConfig {
8027            max_rst_stream_per_window: 1,
8028            ..H2FloodConfig::default()
8029        };
8030        assert_ne!(config_a, config_c);
8031    }
8032
8033    // ── distribute_overhead (additional edge cases) ───────────────────
8034
8035    #[test]
8036    fn test_distribute_overhead_asymmetric_in_out() {
8037        let mut metrics = SessionMetrics::new(None);
8038        let mut overhead_bin = 1000;
8039        let mut overhead_bout = 1000;
8040
8041        // Stream transferred 100% inbound, 0% outbound (not last stream)
8042        distribute_overhead(
8043            &mut metrics,
8044            &mut overhead_bin,
8045            &mut overhead_bout,
8046            (500, 0),   // stream_bytes
8047            (500, 100), // total_bytes
8048            2,          // active_streams
8049            false,      // is_last_stream
8050        );
8051
8052        assert_eq!(metrics.bin, 1000); // 100% of inbound overhead
8053        assert_eq!(metrics.bout, 0); // 0% of outbound overhead
8054        assert_eq!(overhead_bin, 0);
8055        assert_eq!(overhead_bout, 1000);
8056    }
8057
8058    #[test]
8059    fn test_distribute_overhead_many_streams_accumulate() {
8060        let mut metrics = SessionMetrics::new(None);
8061        let mut overhead_bin = 120;
8062        let mut overhead_bout = 120;
8063
8064        // Three equal streams, each calling distribute_overhead.
8065        // With is_last_stream on the third call, the last stream gets all
8066        // remaining overhead, so no rounding loss occurs.
8067        //   call 1: 120 * 100/300 = 40 -> remaining 80
8068        //   call 2:  80 * 100/300 = 26 -> remaining 54
8069        //   call 3: last stream gets all remaining = 54
8070        // Total distributed: 40 + 26 + 54 = 120 (no loss)
8071        for i in 0..3 {
8072            distribute_overhead(
8073                &mut metrics,
8074                &mut overhead_bin,
8075                &mut overhead_bout,
8076                (100, 100), // stream_bytes
8077                (300, 300), // total_bytes
8078                3,          // active_streams
8079                i == 2,     // is_last_stream on final call
8080            );
8081        }
8082
8083        assert_eq!(metrics.bin, 120);
8084        assert_eq!(metrics.bout, 120);
8085        // No rounding residual — last stream absorbed the remainder
8086        assert_eq!(overhead_bin, 0);
8087        assert_eq!(overhead_bout, 0);
8088    }
8089
8090    // ── Hex chunk formatting ────────────────────────────────────────────
8091
8092    /// Verify that the Vec<u8> + write!() hex formatting used in
8093    /// handle_data_frame produces output identical to format!("{:x}").
8094    #[test]
8095    fn test_hex_chunk_length_formatting() {
8096        use std::io::Write as _;
8097
8098        let cases: &[(usize, &[u8])] = &[
8099            (1, b"1"),
8100            (15, b"f"),
8101            (16, b"10"),
8102            (255, b"ff"),
8103            (256, b"100"),
8104            (4096, b"1000"),
8105            (65535, b"ffff"),
8106            (65536, b"10000"),
8107        ];
8108
8109        for &(payload_len, expected) in cases {
8110            let mut buf = Vec::with_capacity(16);
8111            let _ = write!(buf, "{payload_len:x}");
8112            assert_eq!(
8113                buf, expected,
8114                "hex formatting mismatch for payload_len={payload_len}"
8115            );
8116        }
8117
8118        // usize::MAX tested separately to avoid temporary lifetime issue
8119        let max_expected = format!("{:x}", usize::MAX);
8120        let mut buf = Vec::with_capacity(16);
8121        let _ = write!(buf, "{:x}", usize::MAX);
8122        assert_eq!(buf, max_expected.as_bytes());
8123    }
8124
8125    // ── Stream-ID allocation / exhaustion ──────────────────────────────────
8126
8127    /// A fresh client connection starts with `last_stream_id == 0`. The first
8128    /// call MUST issue stream `1` (odd, RFC 9113 §5.1.1) and advance the
8129    /// watermark to `2`.
8130    #[test]
8131    fn test_next_stream_id_client_first_allocation() {
8132        let (issued, next) = next_stream_id(0, true).expect("fresh client must allocate");
8133        assert_eq!(issued, 1);
8134        assert_eq!(next, 2);
8135    }
8136
8137    /// Client allocation yields strictly increasing odd identifiers
8138    /// (1, 3, 5, ...) as required by RFC 9113 §5.1.1.
8139    #[test]
8140    fn test_next_stream_id_client_sequence_is_odd_and_monotonic() {
8141        let mut last = 0u32;
8142        let mut issued_ids = Vec::with_capacity(8);
8143        for _ in 0..8 {
8144            let (id, next) = next_stream_id(last, true).expect("unexhausted");
8145            assert_eq!(id & 1, 1, "client stream ids must be odd (RFC 9113 §5.1.1)");
8146            assert!(issued_ids.last().is_none_or(|prev: &u32| id > *prev));
8147            issued_ids.push(id);
8148            last = next;
8149        }
8150        assert_eq!(issued_ids, vec![1, 3, 5, 7, 9, 11, 13, 15]);
8151    }
8152
8153    /// Server-side allocation yields even identifiers. The helper
8154    /// convention is `watermark - 2` for server, `watermark - 1` for client,
8155    /// so both sides share the same monotonically-increasing even watermark.
8156    /// Sōzu never server-pushes, but the helper must be symmetric so push
8157    /// could be enabled without a regression.
8158    #[test]
8159    fn test_next_stream_id_server_is_even() {
8160        // `last = 2` means the most recent allocation advanced the watermark
8161        // to 2; server then issues `2 - 2 = 0`. This is an artefact of the
8162        // shared watermark and only matters in tests — server never uses it.
8163        let (issued, next) = next_stream_id(2, false).expect("server allocation");
8164        assert_eq!(issued & 1, 0, "server stream ids must be even");
8165        assert_eq!(next, 4);
8166        assert_eq!(issued, 2);
8167
8168        let (issued, next) = next_stream_id(next, false).expect("second slot");
8169        assert_eq!(issued, 4);
8170        assert_eq!(issued & 1, 0);
8171        assert_eq!(next, 6);
8172    }
8173
8174    /// The last client-issuable odd stream ID is `STREAM_ID_MAX = 0x7FFF_FFFF`.
8175    /// To issue it the watermark must advance to `STREAM_ID_MAX + 1 = 2³¹`;
8176    /// the caller therefore supplies `last = STREAM_ID_MAX - 1 = 0x7FFF_FFFE`.
8177    /// That call MUST succeed and return the max ID; the post-call watermark
8178    /// sits at `2³¹`, which is the sentinel that makes the next call fail.
8179    #[test]
8180    fn test_next_stream_id_client_final_slot_allocates() {
8181        let last = STREAM_ID_MAX - 1;
8182        let (issued, next) = next_stream_id(last, true).expect("final slot still allocates");
8183        assert_eq!(issued, STREAM_ID_MAX);
8184        assert_eq!(next, STREAM_ID_MAX + 1);
8185        // And the very next call MUST refuse rather than wrap.
8186        assert!(next_stream_id(next, true).is_none());
8187    }
8188
8189    /// Exhaustion case: once the client has issued stream ID `STREAM_ID_MAX`,
8190    /// the watermark sits at `STREAM_ID_MAX + 1`. The next request MUST return
8191    /// `None` — without this guard the helper would issue `STREAM_ID_MAX + 2`
8192    /// (wrapped down to an even id), which would (a) use the reserved
8193    /// high bit and (b) violate the odd-parity invariant for client streams.
8194    #[test]
8195    fn test_next_stream_id_client_exhausted_returns_none() {
8196        let last = STREAM_ID_MAX + 1;
8197        assert!(next_stream_id(last, true).is_none());
8198    }
8199
8200    /// Exhaustion via `checked_add` saturation: defence in depth in case a
8201    /// caller jumps `last_stream_id` close to `u32::MAX`. The helper must
8202    /// not panic nor overflow — it must return `None`.
8203    #[test]
8204    fn test_next_stream_id_saturates_near_u32_max() {
8205        assert!(next_stream_id(u32::MAX, true).is_none());
8206        assert!(next_stream_id(u32::MAX - 1, true).is_none());
8207    }
8208
8209    /// Server-side exhaustion: same guard, even-parity identifier space.
8210    #[test]
8211    fn test_next_stream_id_server_exhausted_returns_none() {
8212        let last = STREAM_ID_MAX + 1;
8213        assert!(next_stream_id(last, false).is_none());
8214    }
8215
8216    /// Regression guard: the helper must never issue a stream ID that
8217    /// exceeds `STREAM_ID_MAX` for either side, no matter where the
8218    /// watermark sits. This walks every value in a neighbourhood of the
8219    /// boundary to rule out off-by-one errors.
8220    #[test]
8221    fn test_next_stream_id_never_exceeds_stream_id_max() {
8222        for last in (STREAM_ID_MAX - 4)..=(STREAM_ID_MAX + 4) {
8223            for is_client in [true, false] {
8224                if let Some((issued, next)) = next_stream_id(last, is_client) {
8225                    assert!(
8226                        issued <= STREAM_ID_MAX,
8227                        "issued id {issued} exceeds STREAM_ID_MAX (last={last}, is_client={is_client})"
8228                    );
8229                    // `next` is the post-allocation watermark and may sit at
8230                    // STREAM_ID_MAX + 1 — the very next call must then return None.
8231                    if next > STREAM_ID_MAX {
8232                        assert!(
8233                            next_stream_id(next, is_client).is_none(),
8234                            "second call after final slot must report exhaustion"
8235                        );
8236                    }
8237                }
8238            }
8239        }
8240    }
8241
8242    /// The helper's `is_client` flag must cleanly split the ID space so that
8243    /// a client and a server peered on the same connection cannot collide.
8244    /// Given the same `last_stream_id`, the two parities must differ by 1.
8245    #[test]
8246    fn test_next_stream_id_client_server_parities_disjoint() {
8247        for last in [0u32, 2, 4, 10, 100, 1_000_000, STREAM_ID_MAX - 3] {
8248            let (client_id, _) = next_stream_id(last, true).unwrap();
8249            let (server_id, _) = next_stream_id(last, false).unwrap();
8250            assert_eq!(client_id & 1, 1);
8251            assert_eq!(server_id & 1, 0);
8252            assert_eq!(client_id.abs_diff(server_id), 1);
8253        }
8254    }
8255
8256    // ── LIFECYCLE §9 invariant 16: any_stream_id_matches ─────────────────
8257    //
8258    // Covers the iteration dispatch used by `any_stream_has_pending_back`.
8259    // Testing the probe directly against a synthetic closure keeps the
8260    // tests independent of the full `Stream` fixture (which requires a
8261    // `Pool` and a fully-built `HttpContext`).
8262
8263    #[test]
8264    fn test_any_stream_id_matches_empty_map_is_false() {
8265        let streams: HashMap<StreamId, GlobalStreamId> = HashMap::new();
8266        assert!(!any_stream_id_matches(&streams, |_| true));
8267    }
8268
8269    #[test]
8270    fn test_any_stream_id_matches_all_probe_false_is_false() {
8271        let mut streams: HashMap<StreamId, GlobalStreamId> = HashMap::new();
8272        streams.insert(1, 0);
8273        streams.insert(3, 1);
8274        streams.insert(5, 2);
8275        assert!(!any_stream_id_matches(&streams, |_| false));
8276    }
8277
8278    #[test]
8279    fn test_any_stream_id_matches_any_probe_true_is_true() {
8280        let mut streams: HashMap<StreamId, GlobalStreamId> = HashMap::new();
8281        streams.insert(1, 0);
8282        streams.insert(3, 1);
8283        streams.insert(5, 2);
8284        // Probe is true only for GlobalStreamId == 1 (i.e. StreamId 3).
8285        assert!(any_stream_id_matches(&streams, |gid| gid == 1));
8286    }
8287
8288    #[test]
8289    fn test_any_stream_id_matches_single_entry() {
8290        let mut streams: HashMap<StreamId, GlobalStreamId> = HashMap::new();
8291        streams.insert(42, 7);
8292        assert!(any_stream_id_matches(&streams, |gid| gid == 7));
8293        assert!(!any_stream_id_matches(&streams, |gid| gid == 8));
8294    }
8295
8296    #[test]
8297    fn test_any_stream_id_matches_short_circuits() {
8298        let mut streams: HashMap<StreamId, GlobalStreamId> = HashMap::new();
8299        streams.insert(1, 0);
8300        streams.insert(3, 1);
8301        streams.insert(5, 2);
8302        streams.insert(7, 3);
8303        let mut calls = 0usize;
8304        let result = any_stream_id_matches(&streams, |_| {
8305            calls += 1;
8306            true
8307        });
8308        assert!(result);
8309        // `Iterator::any` short-circuits on the first `true` — so the probe
8310        // must fire at most once in this construction.
8311        assert_eq!(calls, 1);
8312    }
8313
8314    // ── cumulative-stall budget decision (fc_stall_budget_decision) ──
8315
8316    #[test]
8317    fn test_fc_stall_budget_open_window_always_clears() {
8318        // A genuinely open send window is a real un-stall, regardless of prior
8319        // accumulated progress or this pass's drain.
8320        assert_eq!(
8321            fc_stall_budget_decision(false, 0, None),
8322            FcStallAction::Clear
8323        );
8324        assert_eq!(
8325            fc_stall_budget_decision(false, 1, Some(5)),
8326            FcStallAction::Clear
8327        );
8328        assert_eq!(
8329            fc_stall_budget_decision(false, i32::MAX, Some(FC_STALL_CLEAR_FLOOR)),
8330            FcStallAction::Clear
8331        );
8332    }
8333
8334    #[test]
8335    fn test_fc_stall_budget_blocked_arms_and_accumulates() {
8336        // First blocked pass arms with this pass's drain.
8337        assert_eq!(
8338            fc_stall_budget_decision(true, 1, None),
8339            FcStallAction::Arm { progress: 1 }
8340        );
8341        // A blocked pass with no drain keeps the accumulator unchanged, so the
8342        // deadline keeps aging (a window-0 stall makes consumed == 0).
8343        assert_eq!(
8344            fc_stall_budget_decision(true, 0, Some(42)),
8345            FcStallAction::Arm { progress: 42 }
8346        );
8347        // Negative `consumed` is clamped to 0 (defensive; converter.window only
8348        // shrinks, so consumed is >= 0 in practice).
8349        assert_eq!(
8350            fc_stall_budget_decision(true, -10, Some(7)),
8351            FcStallAction::Arm { progress: 7 }
8352        );
8353    }
8354
8355    #[test]
8356    fn test_fc_stall_budget_floor_clears() {
8357        // Reaching the floor in a single pass (a full DATA frame of real
8358        // delivery) clears the deadline.
8359        assert_eq!(
8360            fc_stall_budget_decision(true, FC_STALL_CLEAR_FLOOR as i32, None),
8361            FcStallAction::Clear
8362        );
8363        // Exactly one byte below the floor still arms.
8364        assert_eq!(
8365            fc_stall_budget_decision(true, (FC_STALL_CLEAR_FLOOR - 1) as i32, None),
8366            FcStallAction::Arm {
8367                progress: FC_STALL_CLEAR_FLOOR - 1
8368            }
8369        );
8370        // Prior progress plus this pass crossing the floor clears.
8371        assert_eq!(
8372            fc_stall_budget_decision(true, 1, Some(FC_STALL_CLEAR_FLOOR - 1)),
8373            FcStallAction::Clear
8374        );
8375    }
8376
8377    #[test]
8378    fn test_fc_stall_budget_wu_drip_ages_until_floor() {
8379        // The WINDOW_UPDATE(+1) closure: a 1-byte-per-pass drip must keep the
8380        // deadline armed (aging) for the whole run up to the floor and only
8381        // clear on the pass that reaches it — so a drip granting < floor bytes
8382        // per idle period is reaped, never kept alive. This is the unit-level
8383        // proof that the budget closes the WINDOW_UPDATE-drip vector.
8384        let mut progress: Option<usize> = None;
8385        for pass in 1..FC_STALL_CLEAR_FLOOR {
8386            match fc_stall_budget_decision(true, 1, progress) {
8387                FcStallAction::Arm { progress: p } => {
8388                    assert_eq!(p, pass, "drip accumulator off at pass {pass}");
8389                    progress = Some(p);
8390                }
8391                FcStallAction::Clear => panic!("drip cleared the deadline early at pass {pass}"),
8392            }
8393        }
8394        // The pass that reaches the floor finally clears.
8395        assert_eq!(
8396            fc_stall_budget_decision(true, 1, progress),
8397            FcStallAction::Clear
8398        );
8399    }
8400
8401    // ── flow-control-stall reaper union (collect_timed_out_streams) ──
8402
8403    #[test]
8404    fn test_collect_timed_out_streams_reaps_fc_stall_despite_fresh_liveness() {
8405        // A window-stalled stream MUST be reaped on the flow-control-stall
8406        // deadline even if its bidirectional-liveness timer is fresh — an
8407        // inbound 1-byte DATA drip keeps `last_activity` warm but never touches
8408        // `fc_stalled`. Without the `fc_stalled` guard this stream is never
8409        // reaped (the pre-fix window-stall hold).
8410        let now = Instant::now();
8411        let deadline = std::time::Duration::from_secs(2);
8412        let mut live = HashMap::new();
8413        live.insert(7u32, 0usize);
8414        let rst_sent = HashSet::new();
8415        let mut last_activity = HashMap::new();
8416        last_activity.insert(7u32, now); // fresh: just received an inbound DATA drip
8417        let mut fc_stalled = HashMap::new();
8418        fc_stalled.insert(7u32, now - std::time::Duration::from_secs(5));
8419        let out =
8420            collect_timed_out_streams(&last_activity, &fc_stalled, &live, &rst_sent, now, deadline);
8421        assert_eq!(out, vec![(7u32, "H2::WindowStall")]);
8422    }
8423
8424    #[test]
8425    fn test_collect_timed_out_streams_idle_dedup_and_filters() {
8426        let now = Instant::now();
8427        let deadline = std::time::Duration::from_secs(2);
8428        let old = now - std::time::Duration::from_secs(5);
8429        let mut live = HashMap::new();
8430        for sid in [1u32, 3, 5, 9] {
8431            live.insert(sid, 0usize);
8432        }
8433        let mut rst_sent = HashSet::new();
8434        rst_sent.insert(9u32); // already resetting -> excluded
8435        let mut last_activity = HashMap::new();
8436        last_activity.insert(1u32, old); // idle past deadline
8437        last_activity.insert(3u32, now); // fresh -> survives
8438        last_activity.insert(5u32, old); // idle AND fc-stalled -> dedup to one entry
8439        last_activity.insert(9u32, old); // idle but rst_sent -> excluded
8440        last_activity.insert(11u32, old); // not a live stream -> excluded
8441        let mut fc_stalled = HashMap::new();
8442        fc_stalled.insert(5u32, old);
8443        let mut out =
8444            collect_timed_out_streams(&last_activity, &fc_stalled, &live, &rst_sent, now, deadline);
8445        out.sort();
8446        assert_eq!(
8447            out,
8448            vec![(1u32, "H2::IdleTimeout"), (5u32, "H2::IdleTimeout")]
8449        );
8450    }
8451
8452    #[test]
8453    fn test_collect_timed_out_streams_empty_when_all_fresh() {
8454        let now = Instant::now();
8455        let deadline = std::time::Duration::from_secs(2);
8456        let mut live = HashMap::new();
8457        live.insert(1u32, 0usize);
8458        let rst_sent = HashSet::new();
8459        let mut last_activity = HashMap::new();
8460        last_activity.insert(1u32, now);
8461        let mut fc_stalled = HashMap::new();
8462        fc_stalled.insert(1u32, now);
8463        assert!(
8464            collect_timed_out_streams(&last_activity, &fc_stalled, &live, &rst_sent, now, deadline)
8465                .is_empty()
8466        );
8467    }
8468
8469    // ── LIFECYCLE §9 invariant 16: any_stream_has_pending_back ───────────
8470
8471    /// Build a minimal `Stream` for invariant-16 probing. Uses the pool
8472    /// plumbing so `back.blocks` / `back.out` exist; every other field is
8473    /// default-valued because the predicate only reads the back buffer.
8474    fn make_stream_for_invariant_16(pool: &Rc<RefCell<Pool>>, session_ulid: Ulid) -> Stream {
8475        let http_ctx = HttpContext {
8476            keep_alive_backend: true,
8477            keep_alive_frontend: true,
8478            sticky_session_found: None,
8479            method: None,
8480            authority: None,
8481            path: None,
8482            status: None,
8483            reason: None,
8484            user_agent: None,
8485            x_request_id: None,
8486            xff_chain: None,
8487            #[cfg(feature = "opentelemetry")]
8488            otel: None,
8489            closing: false,
8490            session_id: session_ulid,
8491            id: Ulid::generate(),
8492            backend_id: None,
8493            cluster_id: None,
8494            protocol: Protocol::HTTPS,
8495            public_address: "127.0.0.1:0".parse().unwrap(),
8496            session_address: None,
8497            sticky_name: String::new(),
8498            sticky_session: None,
8499            backend_address: None,
8500            tls_server_name: None,
8501            tls_cert_names: None,
8502            strict_sni_binding: false,
8503            elide_x_real_ip: false,
8504            send_x_real_ip: false,
8505            tls_version: None,
8506            tls_cipher: None,
8507            tls_alpn: None,
8508            sozu_id_header: String::from("Sozu-Id"),
8509            redirect_location: None,
8510            www_authenticate: None,
8511            original_authority: None,
8512            headers_response: Vec::new(),
8513            retry_after_seconds: None,
8514            frontend_redirect_template: None,
8515            redirect_status: None,
8516            access_log_message: None,
8517        };
8518        Stream::new(Rc::downgrade(pool), http_ctx, 65_535)
8519            .expect("pool should have capacity for two buffers")
8520    }
8521
8522    fn make_pool_for_invariant_16() -> Rc<RefCell<Pool>> {
8523        // Two buffer slots per stream (front + back), ten stream slots is
8524        // plenty for the tests below.
8525        Rc::new(RefCell::new(Pool::with_capacity(4, 20, 16_384)))
8526    }
8527
8528    #[test]
8529    fn test_any_stream_has_pending_back_empty_map_is_false() {
8530        let pool = make_pool_for_invariant_16();
8531        let ulid = Ulid::generate();
8532        let streams_map: HashMap<StreamId, GlobalStreamId> = HashMap::new();
8533        let context_streams = vec![make_stream_for_invariant_16(&pool, ulid)];
8534        assert!(!any_stream_has_pending_back(&streams_map, &context_streams));
8535    }
8536
8537    #[test]
8538    fn test_any_stream_has_pending_back_all_drained_is_false() {
8539        let pool = make_pool_for_invariant_16();
8540        let ulid = Ulid::generate();
8541        let context_streams = vec![
8542            make_stream_for_invariant_16(&pool, ulid),
8543            make_stream_for_invariant_16(&pool, ulid),
8544        ];
8545        let mut streams_map: HashMap<StreamId, GlobalStreamId> = HashMap::new();
8546        streams_map.insert(1, 0);
8547        streams_map.insert(3, 1);
8548        // Both freshly-built streams have empty back.out and back.blocks
8549        // (Kawa::new starts with empty deques).
8550        assert!(!any_stream_has_pending_back(&streams_map, &context_streams));
8551    }
8552
8553    #[test]
8554    fn test_any_stream_has_pending_back_unknown_gid_is_false() {
8555        // LIFECYCLE invariant 16 defence-in-depth: an unknown
8556        // `GlobalStreamId` during a stream-removal race must not panic;
8557        // `.get()` must short-circuit to `false`.
8558        let pool = make_pool_for_invariant_16();
8559        let ulid = Ulid::generate();
8560        let context_streams = vec![make_stream_for_invariant_16(&pool, ulid)];
8561        let mut streams_map: HashMap<StreamId, GlobalStreamId> = HashMap::new();
8562        // GlobalStreamId 42 is out of range for the 1-element slice above.
8563        streams_map.insert(7, 42);
8564        assert!(!any_stream_has_pending_back(&streams_map, &context_streams));
8565    }
8566
8567    #[test]
8568    fn test_any_stream_has_pending_back_with_pending_blocks_is_true() {
8569        let pool = make_pool_for_invariant_16();
8570        let ulid = Ulid::generate();
8571        let mut stream = make_stream_for_invariant_16(&pool, ulid);
8572        // Push one dummy block — any Block variant is fine; the predicate
8573        // only checks `blocks.is_empty()`.
8574        stream.back.blocks.push_back(kawa::Block::StatusLine);
8575        let mut streams_map: HashMap<StreamId, GlobalStreamId> = HashMap::new();
8576        streams_map.insert(1, 0);
8577        assert!(any_stream_has_pending_back(&streams_map, &[stream]));
8578    }
8579
8580    #[test]
8581    fn test_any_stream_has_pending_back_with_pending_out_is_true() {
8582        let pool = make_pool_for_invariant_16();
8583        let ulid = Ulid::generate();
8584        let mut stream = make_stream_for_invariant_16(&pool, ulid);
8585        // Non-empty out buffer with no blocks.
8586        stream
8587            .back
8588            .out
8589            .push_back(kawa::OutBlock::Store(kawa::Store::Static(b"partial frame")));
8590        let mut streams_map: HashMap<StreamId, GlobalStreamId> = HashMap::new();
8591        streams_map.insert(1, 0);
8592        assert!(any_stream_has_pending_back(&streams_map, &[stream]));
8593    }
8594
8595    // ── ready_incremental_by_urgency mid-pass consistency ────────────────
8596    //
8597    // The full RED is in e2e and currently #[ignore]'d (timing-sensitive).
8598    // The scalar logic below pins the saturating_sub + bucket-scoped
8599    // decrement contract the scheduler at h2.rs:2412-2414 + h2.rs:2481
8600    // relies on: a same-urgency transition-to-ineligible MUST drop the
8601    // per-bucket count by exactly 1 and never underflow the u64.
8602
8603    fn make_bucket(counts: &[(u8, usize)]) -> HashMap<u8, usize> {
8604        counts.iter().copied().collect()
8605    }
8606
8607    #[test]
8608    fn ready_incremental_bucket_decrement_reduces_same_urgency_only() {
8609        let mut map = make_bucket(&[(1, 3), (3, 2)]);
8610        let urgency: u8 = 1;
8611        let is_incremental = true;
8612        // Simulate a stream in urgency=1 going ineligible mid-pass.
8613        if is_incremental && let Some(c) = map.get_mut(&urgency) {
8614            *c = c.saturating_sub(1);
8615        }
8616        assert_eq!(map.get(&1), Some(&2), "urgency-1 bucket must drop to 2");
8617        assert_eq!(map.get(&3), Some(&2), "urgency-3 bucket untouched");
8618    }
8619
8620    #[test]
8621    fn ready_incremental_bucket_decrement_saturates_at_zero() {
8622        let mut map = make_bucket(&[(0, 0)]);
8623        let urgency: u8 = 0;
8624        if let Some(c) = map.get_mut(&urgency) {
8625            *c = c.saturating_sub(1);
8626        }
8627        assert_eq!(map.get(&0), Some(&0), "saturating_sub must not underflow");
8628    }
8629
8630    #[test]
8631    fn ready_incremental_bucket_decrement_skipped_for_non_incremental() {
8632        let mut map = make_bucket(&[(1, 3)]);
8633        let is_incremental = false;
8634        if is_incremental && let Some(c) = map.get_mut(&1) {
8635            *c = c.saturating_sub(1);
8636        }
8637        assert_eq!(
8638            map.get(&1),
8639            Some(&3),
8640            "non-incremental transitions must not touch the bucket"
8641        );
8642    }
8643
8644    // ── enqueue_rst: queue / dedupe / counter / arm invariants ───────────
8645    //
8646    // `enqueue_rst_into` is the free-function primitive shared by all three
8647    // RST push sites (DATA-on-closed, refuse_stream_and_discard,
8648    // reset_stream). The method delegates; the invariants live here.
8649
8650    #[test]
8651    fn test_enqueue_rst_into_populates_queue_and_dedupe() {
8652        let mut pending: Vec<(StreamId, H2Error)> = Vec::new();
8653        let mut total: usize = 0;
8654        let mut sent: HashSet<StreamId> = HashSet::new();
8655        let mut readiness = Readiness::new();
8656
8657        let first = enqueue_rst_into(
8658            &mut pending,
8659            &mut total,
8660            &mut sent,
8661            &mut readiness,
8662            5,
8663            H2Error::ProtocolError,
8664        );
8665        assert!(first, "first call must report freshly_queued = true");
8666        // Second call for the same stream must be a no-op AND return
8667        // false so accounting in `Self::enqueue_rst` skips this case.
8668        let second = enqueue_rst_into(
8669            &mut pending,
8670            &mut total,
8671            &mut sent,
8672            &mut readiness,
8673            5,
8674            H2Error::InternalError,
8675        );
8676        assert!(
8677            !second,
8678            "second call for same stream must return freshly_queued = false"
8679        );
8680
8681        assert_eq!(pending.len(), 1, "dedupe must collapse to a single entry");
8682        assert_eq!(
8683            pending[0],
8684            (5, H2Error::ProtocolError),
8685            "the first error wins — second push is ignored"
8686        );
8687        assert_eq!(total, 1, "queued-cap counter must bump exactly once");
8688        assert!(sent.contains(&5), "rst_sent must record the id");
8689    }
8690
8691    #[test]
8692    fn test_enqueue_rst_into_bumps_total_for_distinct_ids() {
8693        let mut pending: Vec<(StreamId, H2Error)> = Vec::new();
8694        let mut total: usize = 0;
8695        let mut sent: HashSet<StreamId> = HashSet::new();
8696        let mut readiness = Readiness::new();
8697
8698        for sid in [1u32, 3, 5, 7] {
8699            enqueue_rst_into(
8700                &mut pending,
8701                &mut total,
8702                &mut sent,
8703                &mut readiness,
8704                sid,
8705                H2Error::ProtocolError,
8706            );
8707        }
8708
8709        assert_eq!(pending.len(), 4);
8710        assert_eq!(total, 4);
8711        assert_eq!(sent.len(), 4);
8712    }
8713
8714    #[test]
8715    fn test_enqueue_rst_into_arms_writable_in_invariant_15_form() {
8716        let mut pending: Vec<(StreamId, H2Error)> = Vec::new();
8717        let mut total: usize = 0;
8718        let mut sent: HashSet<StreamId> = HashSet::new();
8719        let mut readiness = Readiness::new();
8720
8721        // Precondition: no WRITABLE bits set.
8722        assert!(!readiness.interest.is_writable());
8723        assert!(!readiness.event.is_writable());
8724
8725        enqueue_rst_into(
8726            &mut pending,
8727            &mut total,
8728            &mut sent,
8729            &mut readiness,
8730            9,
8731            H2Error::FlowControlError,
8732        );
8733
8734        // Postcondition: invariant-15 — both `interest` and `event` WRITABLE
8735        // are raised so the next tick runs `writable()` under edge-triggered
8736        // epoll.
8737        assert!(
8738            readiness.interest.is_writable(),
8739            "arm_writable must raise the interest bit"
8740        );
8741        assert!(
8742            readiness.event.is_writable(),
8743            "arm_writable must raise the event bit (edge-triggered epoll)"
8744        );
8745    }
8746
8747    #[test]
8748    fn test_enqueue_rst_into_dedupe_does_not_rearm_writable() {
8749        // Dedupe is a pure short-circuit: if the stream id is already in
8750        // `rst_sent`, we do not touch the readiness. This matters because
8751        // a re-entrant reset_stream call during a cascading error path
8752        // would otherwise re-raise WRITABLE unnecessarily — harmless but
8753        // noisy in metrics.
8754        let mut pending: Vec<(StreamId, H2Error)> = Vec::new();
8755        let mut total: usize = 0;
8756        let mut sent: HashSet<StreamId> = HashSet::new();
8757        sent.insert(11);
8758        let mut readiness = Readiness::new();
8759
8760        enqueue_rst_into(
8761            &mut pending,
8762            &mut total,
8763            &mut sent,
8764            &mut readiness,
8765            11,
8766            H2Error::ProtocolError,
8767        );
8768
8769        assert!(
8770            pending.is_empty(),
8771            "already-sent ids must not queue a second frame"
8772        );
8773        assert_eq!(total, 0);
8774        assert!(!readiness.interest.is_writable());
8775        assert!(!readiness.event.is_writable());
8776    }
8777
8778    // ── forcefully_terminate_answer arms WRITABLE for ET epoll ───────────
8779    //
8780    // Gap A in the h2spec diagnosis: the pre-fix code set `interest` but
8781    // never raised `event`, so `filter_interest() = event & interest` was
8782    // zero and `writable()` was never scheduled. This test pins the fix.
8783
8784    #[test]
8785    fn test_forcefully_terminate_answer_arms_event_and_interest() {
8786        let pool = make_pool_for_invariant_16();
8787        let ulid = Ulid::generate();
8788        let mut stream = make_stream_for_invariant_16(&pool, ulid);
8789        let mut readiness = Readiness::new();
8790
8791        assert!(!readiness.interest.is_writable());
8792        assert!(!readiness.event.is_writable());
8793
8794        forcefully_terminate_answer(&mut stream, &mut readiness, H2Error::ProtocolError);
8795
8796        assert!(
8797            readiness.interest.is_writable(),
8798            "forcefully_terminate_answer must set the WRITABLE interest bit"
8799        );
8800        assert!(
8801            readiness.event.is_writable(),
8802            "forcefully_terminate_answer must set the WRITABLE event bit — \
8803             without this, filter_interest() = 0 under edge-triggered epoll \
8804             and writable() is never scheduled (h2spec Gap A)"
8805        );
8806    }
8807}