Skip to main content

trillium_http/h2/
acceptor.rs

1//! HTTP/2 driver loop ([`H2Driver`]) — owns the per-connection TCP transport and runs the
2//! poll-based state machine that demuxes frames, dispatches stream-opens to handler tasks, and
3//! pumps responses back out.
4//!
5//! Created by [`H2Connection::run`]. The runtime adapter calls [`H2Driver::next`] in a
6//! loop (or drives via the [`Stream`] impl, which has the same semantics); each yield either
7//! returns the next opened request stream (a [`Conn`] for the runtime to spawn a handler
8//! task against) or `None` when the connection is closed.
9//!
10//! The driver is a poll-based state machine, not an async fn. A single `drive` call is the
11//! unit of forward progress: it picks up conn-task signals, advances any in-flight response
12//! sends, drains pending outbound bytes, and advances the read cursor — parking with
13//! cancel-safe partial state when no further progress can be made.
14//!
15//! # Module layout
16//!
17//! Driver impl is split across this file and child modules to keep each focused:
18//!
19//! - **`acceptor.rs`** (this file): struct definition, the [`Self::drive`] orchestration loop, the
20//!   frame read primitive (`poll_fill_to`), and the supporting enums ([`DriverState`],
21//!   [`ReadPhase`], [`CloseOutcome`], [`Action`], [`StreamEntry`]).
22//! - **`acceptor::close`**: the closing lifecycle — entering `Closing` (`begin_close`), the drain
23//!   on `Drained` (`poll_drain_peer`), the teardown when the driver finishes
24//!   (`finish_with_current_outcome`), and [`H2Driver::close`], the transport close that follows.
25//! - **`acceptor::closed_streams`**: bounded ledger of recently-closed streams + reasons, consulted
26//!   to pick the right §5.1 error category for stale peer frames.
27//! - **`acceptor::handler_signals`**: conn-task → driver work-pickup boundary. Owns the
28//!   `needs_servicing` mailbox protocol — `service_handler_signals`, `pick_up_new_client_streams`,
29//!   `has_pending_handler_signals`.
30//! - **`acceptor::outbound`**: outbound write/flush plumbing and `queue_*` frame helpers.
31//! - **`acceptor::recv`**: receive side — frame reader, dispatch, HEADERS+CONTINUATION
32//!   accumulation, malformed-request `RST_STREAM`, DATA routing into per-stream recv rings.
33//! - **`acceptor::send`**: send pump — picks up [`SendCursor`][send::SendCursor]s from the
34//!   conn-task signal pickup, frames HEADERS / DATA / trailing-HEADERS, signals completion.
35//!
36//! [`H2Connection::run`]: super::H2Connection::run
37//! [`Stream`]: futures_lite::stream::Stream
38
39mod close;
40mod closed_streams;
41mod constants;
42mod handler_signals;
43mod inflow;
44mod outbound;
45mod recv;
46mod send;
47#[cfg(test)]
48mod tests;
49mod types;
50
51use super::{H2Error, connection::H2Connection, role::Role, transport::H2Transport};
52use crate::{
53    Conn, Priority,
54    headers::hpack::{HpackDecoder, HpackEncoder},
55};
56use closed_streams::{ClosedReason, ClosedStreams};
57use constants::{
58    INITIAL_CONNECTION_RECV_WINDOW, MAX_BUFFER_SIZE, MAX_DATA_CHUNK_SIZE, MAX_FLOW_CONTROL_WINDOW,
59    READ_WINDOW,
60};
61use futures_lite::io::{AsyncRead, AsyncWrite};
62use hashbrown::HashMap;
63use inflow::Inflow;
64use recv::PendingHeaders;
65use send::ScheduleEntry;
66use std::{
67    collections::BTreeMap,
68    future::Future,
69    io,
70    pin::Pin,
71    sync::Arc,
72    task::{Context, Poll, ready},
73    time::Instant,
74};
75use swansong::ShuttingDown;
76use types::{
77    AcceptorConfig, Action, CloseOutcome, DriverState, Next, ReadPhase, StreamEntry, frame_slice,
78};
79
80/// Owns the per-connection TCP transport and drives the HTTP/2 demux loop.
81///
82/// See the module docs for the high-level driver shape and how its impl is split across the
83/// `recv` and `send` child modules.
84#[derive(Debug)]
85pub struct H2Driver<T> {
86    connection: Arc<H2Connection>,
87    transport: T,
88
89    /// Role this driver runs in — see [`Role`]. Consulted at role-asymmetric branch points
90    /// (preface direction, HEADERS-on-unknown-id, HEADERS-on-known-id).
91    role: Role,
92
93    /// Overall lifecycle position of the driver.
94    state: DriverState,
95
96    /// Future that resolves when the shared `Swansong` begins shutdown. Polled each
97    /// `drive` tick while the driver is running; on resolution the driver queues a
98    /// GOAWAY and transitions to `Closing`, after which the top-of-loop guard returns
99    /// early and we never poll this again on the same acceptor.
100    shutting_down: ShuttingDown,
101
102    /// Inbound bytes, read ahead of the frame being processed. `read_buf[read_start..read_filled]`
103    /// holds everything received but not yet consumed: the current frame (which begins at
104    /// `read_start`), possibly followed by later frames. Survives a return to `Poll::Pending`,
105    /// so a partially received frame keeps its bytes.
106    read_buf: Vec<u8>,
107    read_start: usize,
108    read_filled: usize,
109    read_phase: ReadPhase,
110
111    /// Outbound byte cursor. The driver encodes control frames into `write_buf` and drains
112    /// to the transport via `poll_flush_outbound`. `write_cursor` is the offset of the
113    /// first byte not yet accepted by `poll_write`. After the buffer fully drains, both
114    /// fields are reset and a flush is issued.
115    write_buf: Vec<u8>,
116    write_cursor: usize,
117    write_flush_pending: bool,
118
119    /// HPACK decoder state, shared across all header blocks on this connection.
120    hpack: HpackDecoder,
121
122    /// HPACK encoder state. The driver is the sole owner — handlers / conn tasks
123    /// no longer touch it, so this is a plain field with no synchronization.
124    hpack_encoder: HpackEncoder,
125
126    /// Per-stream state, keyed by stream id. Driver-only — handler tasks hold their own
127    /// `Arc<StreamState>` via [`H2Transport`] and don't consult this table. The entry
128    /// bundles the shared state with driver-private bookkeeping (e.g. "have we already
129    /// advertised the recv window after seeing `is_reading`?").
130    streams: BTreeMap<u32, StreamEntry>,
131
132    /// Highest peer-initiated stream id seen so far. Peer-initiated (client) stream ids
133    /// must be odd and strictly increasing.
134    last_peer_stream_id: u32,
135
136    /// Latest RFC 9218 priority signaled per stream via `PRIORITY_UPDATE`, keyed by stream
137    /// id. Holds entries for streams not yet opened too (an update can precede its HEADERS),
138    /// so it is bounded by [`recv::MAX_TRACKED_PRIORITIES`] as a `DoS` guard. Latest signal
139    /// wins; the request's own `priority` header (on the [`StreamEntry`]) is the fallback when
140    /// no entry is present. See [`Self::effective_priority`].
141    ///
142    /// Lookups are by stream id and nothing iterates this in order, so it's a hash map.
143    stream_priorities: HashMap<u32, Priority>,
144
145    /// Reused scratch for the send pump's priority schedule ([`ScheduleEntry`] per sending
146    /// stream), rebuilt and sorted each pump tick. Kept on the driver (rather than allocated per
147    /// tick) so the per-tick cost on this hot path is a clear + extend, not a fresh allocation.
148    /// See [`Self::advance_outbound_sends`].
149    send_schedule: Vec<ScheduleEntry>,
150
151    /// Ids of the streams holding a send cursor, which are the only streams the send pump has
152    /// work for. Scheduling from this rather than from every open stream keeps a pump tick
153    /// proportional to the responses in flight: with many concurrent streams, most are waiting
154    /// on their handlers. Pushed when a cursor is installed; ids whose cursor has since been
155    /// dropped are pruned after each pump tick.
156    sending_streams: Vec<u32>,
157
158    /// Reused buffer for the stream ids taken off the connection's signal queue each tick. Empty
159    /// between ticks.
160    signaled_scratch: Vec<u32>,
161
162    /// Accumulator for an in-progress HEADERS block that is waiting on further CONTINUATION
163    /// frames. `None` outside a HEADERS block. The spec forbids any frame on any stream
164    /// from interleaving while this is `Some`.
165    pending_headers: Option<PendingHeaders>,
166
167    /// Set once the driver decides to close: graceful (peer GOAWAY / server swansong / peer
168    /// EOF) or erroring (protocol violation → GOAWAY with code, or I/O failure → no
169    /// GOAWAY). `drive` completes (returns `None` or a final `Some(Err(...))`) once
170    /// outbound drains to empty.
171    close_outcome: Option<CloseOutcome>,
172
173    /// Set after `drive` yields its terminal result. Subsequent calls return `None` without
174    /// touching the transport.
175    finished: bool,
176
177    /// Reusable scratch the send pump reads body chunks into before framing as DATA.
178    /// Sized at [`MAX_DATA_CHUNK_SIZE`] — even if the peer permits larger frames we cap our
179    /// DATA emissions here to bound per-connection memory.
180    body_scratch: Vec<u8>,
181
182    /// Reusable scratch the HPACK encoder writes a HEADERS block into before it is copied
183    /// into `write_buf` as HEADERS/CONTINUATION fragments. Retained across responses so the
184    /// steady-state header encode allocates nothing.
185    headers_scratch: Vec<u8>,
186
187    /// Connection-level send flow-control window. Tracked as [`i64`] for symmetry with the
188    /// per-stream windows, which a mid-connection `INITIAL_WINDOW_SIZE` reduction can drive
189    /// temporarily negative; the connection window itself is *not* affected by
190    /// `SETTINGS_INITIAL_WINDOW_SIZE`. Decremented as we emit DATA; incremented by peer
191    /// `WINDOW_UPDATE(stream_id=0, inc)`. Overflow past [`MAX_FLOW_CONTROL_WINDOW`] is a
192    /// connection-level `FLOW_CONTROL_ERROR`.
193    connection_send_window: i64,
194
195    /// Connection-level receive flow-control window. Starts at the spec's 65535-octet baseline
196    /// (the spec forbids SETTINGS from altering it) and is promoted to the configured
197    /// `h2_initial_connection_window_size` via an initial `WINDOW_UPDATE(0)` right after SETTINGS,
198    /// then topped up as handlers drain across all streams. See [`Inflow`]. A peer that sends past
199    /// the granted window earns a connection-level `FLOW_CONTROL_ERROR`.
200    connection_inflow: Inflow,
201
202    /// Bounded ledger of recently-closed streams and why they closed. Consulted by
203    /// [`recv::H2Driver::finalize_headers`] when a HEADERS frame arrives on an id ≤
204    /// `last_peer_stream_id` that's not in the active map, to distinguish `RST_STREAM`-
205    /// closed (stream-level `STREAM_CLOSED`) from `END_STREAM`-closed or never-opened
206    /// (connection-level). See [`ClosedStreams`] for the eviction policy.
207    closed_streams: ClosedStreams,
208
209    /// Snapshot of the h2-relevant fields of [`HttpConfig`][crate::HttpConfig] taken at
210    /// acceptor construction. Copied in because `HttpConfig` is per-server but an acceptor
211    /// is per-connection — the config is effectively immutable over a connection's
212    /// lifetime, and a local copy avoids reaching through [`H2Connection::context`] on
213    /// every policy check.
214    ///
215    /// [`H2Connection::context`]: super::H2Connection::context
216    pub(super) config: AcceptorConfig,
217
218    /// The start time shared by streams opened from the latest transport read: read from the
219    /// clock when the first of them opens, and reset by every read. A burst of new streams reads
220    /// the clock once, and no stream's start time predates the read that completed its HEADERS.
221    burst_start: Option<Instant>,
222
223    /// Rounds of `drive` work since it last returned `Pending`. Carried across calls because
224    /// emitting a stream ends a call with `Ready`: without it, a burst of new streams would never
225    /// reach the cooperative-yield bound, and every stream in the burst would be built before any
226    /// of their handlers ran, by which time the first streams' state has gone cold.
227    rounds_since_pending: usize,
228}
229
230impl<T> H2Driver<T>
231where
232    T: AsyncRead + AsyncWrite + Unpin + Send,
233{
234    pub(super) fn new(connection: Arc<H2Connection>, transport: T, role: Role) -> Self {
235        let shutting_down = connection.swansong().shutting_down();
236        let context = connection.context();
237        let config = AcceptorConfig::from_http_config(context.config());
238        let hpack_encoder = HpackEncoder::new(
239            context.observer.clone(),
240            context.config.dynamic_table_capacity(),
241            context.config.recent_pairs_size(),
242            context.config.recent_pairs_auto(),
243        );
244        Self {
245            connection,
246            transport,
247            role,
248            state: DriverState::AwaitingPreface,
249            shutting_down,
250            read_buf: Vec::new(),
251            read_start: 0,
252            read_filled: 0,
253            read_phase: ReadPhase::NeedHeader,
254            write_buf: Vec::new(),
255            write_cursor: 0,
256            write_flush_pending: false,
257            hpack: HpackDecoder::new(config.hpack_table_capacity()),
258            hpack_encoder,
259            streams: BTreeMap::new(),
260            last_peer_stream_id: 0,
261            stream_priorities: HashMap::new(),
262            send_schedule: Vec::new(),
263            sending_streams: Vec::new(),
264            signaled_scratch: Vec::new(),
265            pending_headers: None,
266            close_outcome: None,
267            finished: false,
268            body_scratch: vec![0u8; MAX_DATA_CHUNK_SIZE as usize],
269            headers_scratch: Vec::new(),
270            connection_send_window: INITIAL_CONNECTION_RECV_WINDOW,
271            connection_inflow: Inflow::new(INITIAL_CONNECTION_RECV_WINDOW),
272            closed_streams: ClosedStreams::default(),
273            config,
274            burst_start: None,
275            rounds_since_pending: 0,
276        }
277    }
278
279    /// The shared [`H2Connection`] this acceptor was created from.
280    pub fn connection(&self) -> &Arc<H2Connection> {
281        &self.connection
282    }
283
284    /// Drive the connection until the next request stream opens, the connection ends, or a
285    /// fatal protocol or I/O error occurs.
286    ///
287    /// Returns `Ok(Some(conn))` for each new request stream — the runtime adapter is
288    /// expected to spawn a handler task that consumes the [`Conn`]. Malformed requests are
289    /// handled internally with a stream-level `RST_STREAM` and never surfaced. Returns
290    /// `Ok(None)` when the connection has been shut down cleanly (peer GOAWAY, our own
291    /// swansong shutdown, peer EOF at a frame boundary).
292    ///
293    /// # Errors
294    ///
295    /// The returned future resolves to an [`H2Error`] for any *connection-level* protocol
296    /// violation detected while decoding peer frames or for an unrecoverable transport I/O
297    /// error. A final GOAWAY is sent before a protocol error is returned (best-effort; I/O
298    /// errors skip it).
299    // Mirrors `StreamExt::next` (a `&mut self -> impl Future<Output = Option<T>>` adapter),
300    // not `Iterator::next`. The driver is also `Stream`, so callers can use either.
301    #[allow(clippy::should_implement_trait)]
302    pub fn next(&mut self) -> Next<'_, T> {
303        Next { driver: self }
304    }
305
306    /// Poll-based driver core. Shared by [`Next`]'s `Future` impl, the [`Stream`] impl on
307    /// [`H2Driver`], and [`H2Initiator`][super::H2Initiator]'s client-side Future impl.
308    ///
309    /// [`Stream`]: futures_lite::stream::Stream
310    pub(super) fn drive(
311        &mut self,
312        cx: &mut Context<'_>,
313    ) -> Poll<Option<Result<Conn<H2Transport>, H2Error>>> {
314        let poll = self.drive_rounds(cx);
315        if poll.is_pending() {
316            self.rounds_since_pending = 0;
317        }
318        poll
319    }
320
321    #[allow(
322        clippy::too_many_lines,
323        reason = "state-machine orchestration; splitting muddies the read-as-a-recipe shape"
324    )]
325    fn drive_rounds(
326        &mut self,
327        cx: &mut Context<'_>,
328    ) -> Poll<Option<Result<Conn<H2Transport>, H2Error>>> {
329        if self.finished {
330            return Poll::Ready(None);
331        }
332
333        while self.rounds_since_pending < self.config.copy_loops_per_yield() {
334            self.rounds_since_pending += 1;
335            log::trace!("h2 drive loop number: {}", self.rounds_since_pending);
336            // 1. Conn-task signals. Picks up window-update intent (`is_reading`) and new
337            //    `submit_send` submissions, moving them into driver-private state.
338            self.service_handler_signals();
339
340            // 2. Send pump. Turns picked-up SendCursors into HEADERS / DATA / trailing- HEADERS
341            //    frame bytes in `write_buf`. Body reads that return Pending leave the cursor in
342            //    place — the body's source will wake the driver task.
343            self.advance_outbound_sends(cx);
344
345            // 3. Flush any pending outbound — never re-poll reads when we still owe bytes to the
346            //    peer, and never signal closure to the caller before the wire is clean.
347            match self.poll_flush_outbound(cx) {
348                Poll::Ready(Ok(())) => {}
349                Poll::Ready(Err(e)) => {
350                    // Flush failure while closing: just take whatever outcome we had and
351                    // shelve the fresh I/O error. While running, record and finish.
352                    if self.close_outcome.is_none() {
353                        self.close_outcome = Some(CloseOutcome::Io(e));
354                    }
355                    return Poll::Ready(self.finish_with_current_outcome());
356                }
357                Poll::Pending => return Poll::Pending,
358            }
359
360            // 4. If we were closing, outbound is now drained. For graceful (or protocol-error)
361            //    shutdowns, transition to `Drained`, which reads whatever the peer has already sent
362            //    before finishing. For I/O-error shutdowns the transport is already untrustworthy,
363            //    so skip the drain. Defer the transition while in-flight streams still have
364            //    outbound (an active SendCursor or queued parts), an open send half (a handler that
365            //    hasn't submitted its response yet — half-closed-remote is *not* drained), OR
366            //    inbound (recv half not yet closed) work. Without this, a handler that submits
367            //    trailers *after* the cancellation race resolves gets stranded with bytes parked in
368            //    mailboxes; a handler that hasn't responded yet when shutdown begins has its
369            //    response `SubmitSend` orphaned by a driver that finished out from under it; and a
370            //    client receiving GOAWAY mid-stream stops decoding incoming frames before the
371            //    server's trailing HEADERS arrive. Falls through to step 6 so the recv pump (also
372            //    gated on Running|Closing now) keeps running and parks on the transport read waker
373            //    rather than the outbound-only `park` here.
374            if self.state == DriverState::Closing {
375                if matches!(self.close_outcome, Some(CloseOutcome::Io(_))) {
376                    return Poll::Ready(self.finish_with_current_outcome());
377                }
378                if self.has_active_send_cursors()
379                    || self.has_open_send_half()
380                    || self.has_pending_recv()
381                {
382                    self.log_closing_blockers();
383                } else {
384                    self.set_state(
385                        DriverState::Drained,
386                        "outbound drained, no in-flight streams",
387                    );
388                }
389            }
390
391            // 5. Server-initiated shutdown check. Only relevant while we're running — once we're
392            //    past the Closing/Drained transition we've already committed to a close and
393            //    re-observing the swansong here would re-enter begin_close in a loop. Post-shutdown
394            //    re-polls of `ShuttingDown` are harmless themselves (event_listener-backed, not
395            //    single-shot) but the re-entry isn't.
396            if self.state == DriverState::Running
397                && Pin::new(&mut self.shutting_down).poll(cx).is_ready()
398            {
399                self.begin_close(CloseOutcome::Graceful);
400                continue;
401            }
402
403            // 6. State-specific step.
404            match self.state {
405                DriverState::AwaitingPreface => {
406                    // Role-asymmetric: server reads the 24-byte preface off the wire; client
407                    // writes it to `write_buf` (the next drain tick flushes it, then our
408                    // SETTINGS, then the peer's SETTINGS arrives as the first frame in Running).
409                    let poll = match self.role {
410                        Role::Server => self.poll_read_preface(cx),
411                        Role::Client => {
412                            self.queue_client_preface();
413                            Poll::Ready(Ok(()))
414                        }
415                    };
416                    match poll {
417                        Poll::Ready(Ok(())) => {
418                            self.set_state(DriverState::NeedsServerSettings, "preface complete");
419                        }
420                        Poll::Ready(Err(e)) => {
421                            self.close_outcome = Some(e);
422                            return Poll::Ready(self.finish_with_current_outcome());
423                        }
424                        Poll::Pending => {
425                            if self.park(cx) {
426                                return Poll::Pending;
427                            }
428                        }
429                    }
430                }
431
432                DriverState::NeedsServerSettings => {
433                    self.queue_settings();
434                    // The spec forbids SETTINGS from altering the connection-level
435                    // flow-control window — it stays at the 65535 baseline unless we raise
436                    // it via `WINDOW_UPDATE(0)`. Do that immediately after SETTINGS so peer
437                    // bulk uploads aren't capped at ~5 Mbit/s × RTT.
438                    let raise = self
439                        .connection_inflow
440                        .raise_target(i64::from(self.config.initial_connection_window_size()));
441                    if raise > 0 {
442                        self.queue_window_update(0, u32::try_from(raise).unwrap_or(u32::MAX));
443                    }
444                    self.set_state(DriverState::Running, "initial SETTINGS queued");
445                }
446
447                // Read pump runs in both Running and Closing so a Closing-side driver
448                // (we sent or received GOAWAY) keeps decoding inbound frames for streams
449                // that haven't reached recv-closed yet — e.g. trailing HEADERS for an
450                // in-flight server-stream the peer is about to send. New `Action::Emit`
451                // streams are ignored in Closing: post-GOAWAY the peer shouldn't be
452                // opening new ones (and we wouldn't want to dispatch handlers for them
453                // even if it did).
454                DriverState::Running | DriverState::Closing => match self.poll_advance_read(cx) {
455                    Poll::Ready(Ok(Action::Continue)) => {}
456                    Poll::Ready(Ok(Action::Emit(conn))) => {
457                        if self.state == DriverState::Running {
458                            return Poll::Ready(Some(Ok(conn)));
459                        }
460                        // Closing — drop the conn; outer loop continues processing
461                        // remaining in-flight streams until drained.
462                    }
463                    Poll::Ready(Ok(Action::Close(outcome))) => {
464                        self.begin_close(outcome);
465                    }
466                    // Protocol errors need a GOAWAY on the wire before we terminate;
467                    // `begin_close` queues that and transitions us to Closing so the next
468                    // outer-loop iteration drains the frame. Io errors short-circuit:
469                    // if we're already Closing, the transport is gone, so finish without
470                    // looping forever waiting for in-flight streams (`has_pending_recv`
471                    // can't decide on its own that the peer is never sending again).
472                    Poll::Ready(Err(e)) => {
473                        if self.state == DriverState::Closing {
474                            self.close_outcome.get_or_insert(e);
475                            return Poll::Ready(self.finish_with_current_outcome());
476                        }
477                        self.begin_close(e);
478                    }
479                    Poll::Pending => {
480                        if self.park(cx) {
481                            return Poll::Pending;
482                        }
483                    }
484                },
485
486                DriverState::Drained => match self.poll_drain_peer(cx) {
487                    Poll::Ready(()) => {
488                        return Poll::Ready(self.finish_with_current_outcome());
489                    }
490                    Poll::Pending => return Poll::Pending,
491                },
492            }
493        }
494
495        // Cooperative yield: we made `copy_loops_per_yield` rounds of progress without
496        // hitting an internal Pending, counting rounds that ended a call by emitting a stream.
497        // Re-arm immediately and let the runtime pick up anything else it has waiting —
498        // notably the handler tasks for the streams just emitted — before we resume.
499        cx.waker().wake_by_ref();
500        Poll::Pending
501    }
502
503    /// Register the driver's waker with the shared `outbound_waker` (so handler tasks can
504    /// wake the driver) and tell the caller whether it's safe to park. Returns `true` if
505    /// the driver should return `Poll::Pending`, or `false` if a handler produced work
506    /// between our last check and the registration — in which case the caller should loop
507    /// around to pick it up.
508    fn park(&mut self, cx: &mut Context<'_>) -> bool {
509        self.connection.outbound_waker().register(cx.waker());
510        !self.has_pending_handler_signals() && !self.has_pending_outbound_progress()
511    }
512
513    /// The sole mutator of `self.state`. Logs every transition so a trace log reads as
514    /// a sequence of named lifecycle events.
515    fn set_state(&mut self, new: DriverState, reason: &'static str) {
516        if self.state == new {
517            return;
518        }
519        log::trace!(
520            "h2 driver: state {old:?} → {new:?} ({reason})",
521            old = self.state,
522        );
523        self.state = new;
524    }
525
526    /// Make the first `target` bytes of the current frame available at `read_buf[read_start..]`,
527    /// reading from the transport as needed. Each read takes whatever the transport has, up to
528    /// the free space, so it can also pick up frames that follow. Cancel-safe: if the caller
529    /// drops the Future, bytes already read stay buffered.
530    ///
531    /// A 0-byte read is surfaced as `UnexpectedEof`. The caller maps this to a terminal
532    /// I/O error; we don't emit a GOAWAY on peer-initiated close.
533    fn poll_fill_to(&mut self, target: usize, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
534        while self.read_filled - self.read_start < target {
535            if self.read_start > 0 {
536                // Slide the partial frame to the front so the read gets all the free space.
537                self.read_buf
538                    .copy_within(self.read_start..self.read_filled, 0);
539                self.read_filled -= self.read_start;
540                self.read_start = 0;
541            }
542            let len = target.max(READ_WINDOW);
543            if self.read_buf.len() < len {
544                self.read_buf.resize(len, 0);
545            }
546            let n = ready!(
547                Pin::new(&mut self.transport).poll_read(cx, &mut self.read_buf[self.read_filled..])
548            )?;
549            if n == 0 {
550                return Poll::Ready(Err(io::Error::from(io::ErrorKind::UnexpectedEof)));
551            }
552            self.read_filled += n;
553            self.burst_start = None;
554        }
555        Poll::Ready(Ok(()))
556    }
557
558    /// Consume `len` bytes from the front of the buffered input: a dispatched frame, or the
559    /// connection preface.
560    fn consume_read(&mut self, len: usize) {
561        self.read_start += len;
562        self.read_phase = ReadPhase::NeedHeader;
563        if self.read_start == self.read_filled {
564            self.read_start = 0;
565            self.read_filled = 0;
566            // Release a buffer that ballooned for a big frame.
567            if self.read_buf.capacity() > MAX_BUFFER_SIZE / 16 {
568                self.read_buf = Vec::new();
569            }
570        }
571    }
572
573    /// Look up why a stream is closed. `None` means either never-opened or evicted from the
574    /// bounded ledger — both fall through to the connection-level default.
575    pub(super) fn closed_reason(&self, stream_id: u32) -> Option<ClosedReason> {
576        self.closed_streams.reason(stream_id)
577    }
578}