Skip to main content

trillium_http/h2/acceptor/
types.rs

1//! Component types used in the definition of `h2::acceptor`
2
3use crate::{
4    Conn, HttpConfig, Priority,
5    h2::{
6        H2Driver, H2Error, H2ErrorCode, H2Transport,
7        acceptor::{inflow::Inflow, send::SendCursor},
8        transport::StreamState,
9    },
10};
11use futures_lite::{AsyncRead, AsyncWrite, stream::Stream};
12use std::{
13    io,
14    pin::Pin,
15    sync::Arc,
16    task::{Context, Poll},
17};
18
19/// h2-relevant configuration extracted from [`HttpConfig`][crate::HttpConfig] at acceptor
20/// construction. Carried as a plain value so hot-loop policy checks don't cross the
21/// `Arc<HttpContext>` indirection.
22#[derive(Debug, Clone, Copy, fieldwork::Fieldwork)]
23#[fieldwork(get)]
24pub(in crate::h2) struct AcceptorConfig {
25    initial_stream_window_size: u32,
26    max_stream_recv_window_size: u32,
27    initial_connection_window_size: u32,
28    max_concurrent_streams: u32,
29    max_frame_size: u32,
30    max_header_list_size: u64,
31    copy_loops_per_yield: usize,
32    hpack_table_capacity: usize,
33}
34
35impl AcceptorConfig {
36    pub(super) fn from_http_config(config: &HttpConfig) -> Self {
37        let initial_stream_window_size = config.h2_initial_stream_window_size();
38        let configured_max = config.h2_max_stream_recv_window_size();
39        // The post-read window target must be at least the advertised initial: a smaller `max`
40        // would leave the stream stuck above its own "max" (promotion via `Inflow::raise_target`
41        // can only grow the window, never shrink it). Coerce it up and warn once so the
42        // misconfiguration is visible without spamming the log on every connection.
43        let max_stream_recv_window_size = configured_max.max(initial_stream_window_size);
44        if max_stream_recv_window_size != configured_max {
45            warn_misordered_stream_window_config(initial_stream_window_size, configured_max);
46        }
47        Self {
48            initial_stream_window_size,
49            max_stream_recv_window_size,
50            initial_connection_window_size: config.h2_initial_connection_window_size(),
51            max_concurrent_streams: config.h2_max_concurrent_streams(),
52            max_frame_size: config.h2_max_frame_size(),
53            max_header_list_size: config.max_header_list_size(),
54            copy_loops_per_yield: config.copy_loops_per_yield(),
55            hpack_table_capacity: config.dynamic_table_capacity(),
56        }
57    }
58}
59
60/// Warn (at most once per process) that `h2_max_stream_recv_window_size` was configured below
61/// `h2_initial_stream_window_size` and has been clamped up to it. Best-effort once — a second
62/// differently-misconfigured `HttpConfig` won't re-warn — which is fine for a setup-time hint.
63fn warn_misordered_stream_window_config(initial: u32, configured_max: u32) {
64    use std::sync::atomic::{AtomicBool, Ordering};
65    static WARNED: AtomicBool = AtomicBool::new(false);
66    if !WARNED.swap(true, Ordering::Relaxed) {
67        log::warn!(
68            "h2_max_stream_recv_window_size ({configured_max}) is below \
69             h2_initial_stream_window_size ({initial}); clamping the per-stream recv window up to \
70             the initial. Set max >= initial to silence this."
71        );
72    }
73}
74
75/// Position of the connection in its high-level lifecycle.
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub(super) enum DriverState {
78    /// Haven't read the client preface yet.
79    AwaitingPreface,
80    /// Preface read; need to queue our initial SETTINGS frame to `write_buf`.
81    NeedsServerSettings,
82    /// Steady state — read frames from the transport and dispatch.
83    Running,
84    /// GOAWAY has been queued; drain `write_buf` then transition to [`Drained`] (for
85    /// graceful shutdown) or terminate directly (for I/O error paths where the transport
86    /// is already untrustworthy).
87    ///
88    /// [`Drained`]: Self::Drained
89    Closing,
90    /// Our outbound bytes are on the wire (including our GOAWAY). Now we're waiting for
91    /// the peer to close its write half (recv returns 0) so our Drop doesn't look like a
92    /// reset to the client — the sequence the h2 spec and most clients assume. Any
93    /// inbound bytes the peer happens to send during this window are discarded; we've
94    /// already committed to closing.
95    Drained,
96}
97
98/// Where the read cursor is inside the current frame.
99#[derive(Debug, Clone, Copy)]
100pub(super) enum ReadPhase {
101    /// Not yet read the 9 bytes of the next frame header.
102    NeedHeader,
103    /// Header read and validated; still collecting payload bytes. `total` is the full target
104    /// fill (`FRAME_HEADER_LEN + payload_len`). The decoded header itself is cheap enough to
105    /// re-parse from the buffer when we dispatch, so we don't stash it here.
106    NeedPayload { total: usize },
107}
108
109/// Why the driver is closing — shaped around what the terminal `drive` result should be.
110#[derive(Debug)]
111pub(super) enum CloseOutcome {
112    /// Clean close. `drive` returns `None`.
113    Graceful,
114    /// Protocol error. `drive` returns `Some(Err(...))`. GOAWAY with this code has been
115    /// queued.
116    Protocol(H2ErrorCode),
117    /// I/O error. GOAWAY was NOT queued (transport is untrustworthy). Propagated verbatim.
118    Io(io::Error),
119}
120
121/// Driver-side view of a single open stream: the shared state the handler also sees, plus a
122/// cache of decisions the driver has made for this stream (which the handler doesn't need
123/// to know).
124#[derive(Debug)]
125pub(super) struct StreamEntry {
126    /// Shared state (recv buffer, send slot, handler wakers). Owned by `Arc` so the
127    /// handler task can outlive or operate concurrently with the driver's view.
128    pub(super) shared: Arc<StreamState>,
129
130    /// Driver-private send-side state for an in-progress response. `None` until the conn
131    /// task submits a response via [`H2Connection::submit_send`] and the driver picks it
132    /// up on its next `service_handler_signals` tick.
133    ///
134    /// [`H2Connection::submit_send`]: super::H2Connection::submit_send
135    pub(super) send: Option<SendCursor>,
136
137    /// Per-stream send flow-control window. Seeded from
138    /// `peer_settings.effective_initial_window_size()` when the stream is opened;
139    /// decremented as we emit DATA frames; incremented by peer
140    /// `WINDOW_UPDATE(stream_id, inc)`; adjusted by `SETTINGS_INITIAL_WINDOW_SIZE` delta on
141    /// mid-connection SETTINGS change (may drive negative). Overflow past
142    /// [`MAX_FLOW_CONTROL_WINDOW`] is a stream-level `FLOW_CONTROL_ERROR` (→ `RST_STREAM`).
143    pub(super) send_window: i64,
144
145    /// Per-stream receive flow-control window (RFC 9113 §6.9). Seeded at the advertised
146    /// `SETTINGS_INITIAL_WINDOW_SIZE`; promoted to `h2_max_stream_recv_window_size` once the
147    /// handler signals it intends to read the body (`is_reading`), then topped up as the handler
148    /// drains. See [`Inflow`] for the accounting model. A peer that sends past the granted window
149    /// earns a connection-level `FLOW_CONTROL_ERROR`.
150    pub(super) stream_inflow: Inflow,
151
152    /// Declared request-body length from the `content-length` request header, if present and
153    /// parseable. The driver tallies inbound DATA octets against this to enforce RFC 9113
154    /// §8.1.2.6: a body whose length disagrees with `content-length` is a stream-level
155    /// `PROTOCOL_ERROR`. `None` means no declared length, so no check applies.
156    pub(super) expected_content_length: Option<u64>,
157
158    /// Running total of inbound DATA payload octets (body bytes only — pad-length byte and
159    /// padding excluded) received on this stream, compared against `expected_content_length`.
160    pub(super) received_body_len: u64,
161
162    /// RFC 9218 priority parsed from the request's `priority` header at stream open (or the
163    /// default when absent). The send pump's fallback when no `PRIORITY_UPDATE` has overridden
164    /// it — see [`H2Driver::effective_priority`][super::H2Driver::effective_priority]. Always
165    /// the default on client-role streams, which carry no scheduling signal of their own.
166    pub(super) priority: Priority,
167}
168
169impl StreamEntry {
170    pub(super) fn new(
171        shared: Arc<StreamState>,
172        send_window: i64,
173        stream_inflow: Inflow,
174        expected_content_length: Option<u64>,
175        priority: Priority,
176    ) -> Self {
177        Self {
178            shared,
179            send: None,
180            send_window,
181            stream_inflow,
182            expected_content_length,
183            received_body_len: 0,
184            priority,
185        }
186    }
187}
188
189/// Result of dispatching one decoded frame.
190pub(super) enum Action {
191    /// Frame handled; continue the main loop.
192    Continue,
193    /// A stream just opened and the request validated — return the [`Conn`] to the caller;
194    /// the runtime adapter spawns a handler task per emitted Conn. Boxed to keep the enum
195    /// small — `Conn` is over 500 bytes and most dispatches return `Continue`.
196    Emit(Box<Conn<H2Transport>>),
197    /// Begin graceful or erroring close with this outcome.
198    Close(CloseOutcome),
199}
200
201/// Future returned by [`H2Driver::next`]. Resolves to `None` on graceful close, `Some(Ok)`
202/// when a new request stream opens, or `Some(Err)` on a fatal protocol or I/O error.
203#[must_use = "futures do nothing unless awaited"]
204#[derive(Debug)]
205pub struct Next<'a, T> {
206    pub(super) driver: &'a mut H2Driver<T>,
207}
208
209impl<T> Future for Next<'_, T>
210where
211    T: AsyncRead + AsyncWrite + Unpin + Send,
212{
213    type Output = Option<Result<Conn<H2Transport>, H2Error>>;
214
215    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
216        self.driver.drive(cx)
217    }
218}
219
220impl<T> Stream for H2Driver<T>
221where
222    T: AsyncRead + AsyncWrite + Unpin + Send,
223{
224    type Item = Result<Conn<H2Transport>, H2Error>;
225
226    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
227        self.get_mut().drive(cx)
228    }
229}
230
231/// Slice the interesting bytes out of a just-read frame. Bounds-checks to defend against a
232/// payload length on the wire that disagrees with a body-bearing frame's declared inner
233/// length.
234pub(super) fn frame_slice(
235    buf: &[u8],
236    start: usize,
237    length: u32,
238    total: usize,
239) -> Result<&[u8], CloseOutcome> {
240    let length =
241        usize::try_from(length).map_err(|_| CloseOutcome::Protocol(H2ErrorCode::FrameSizeError))?;
242    let end = start
243        .checked_add(length)
244        .ok_or(CloseOutcome::Protocol(H2ErrorCode::FrameSizeError))?;
245    if end > total {
246        return Err(CloseOutcome::Protocol(H2ErrorCode::FrameSizeError));
247    }
248    Ok(&buf[start..end])
249}