Skip to main content

h2ts_client/
connection.rs

1//! The HTTP/2 connection — port of `connection.ts` (+ `stream.ts`, `types.ts`).
2//!
3//! Owns the [`Transport`], drives read/write loops, multiplexes streams, and
4//! implements the request/response flow (RFC 7540 §5–6). Opens with the
5//! connection preface + SETTINGS and issues the first request immediately —
6//! prior knowledge, no `Upgrade` round-trip.
7//!
8//! Faithful to the single-threaded JS object model via `Rc<RefCell<_>>`; the read
9//! loop and the (channel-serialized) write loop run as one [`connect`]-returned
10//! driver future the caller spawns.
11//!
12//! Deferred from the TS (marked `TODO`): server-push callbacks (pushes are
13//! refused) and abort signals.
14
15use std::cell::RefCell;
16use std::collections::{HashMap, VecDeque};
17use std::future::Future;
18use std::pin::Pin;
19use std::rc::{Rc, Weak};
20use std::task::{Context, Poll, Waker};
21
22use futures::channel::{mpsc, oneshot};
23use futures::future::{poll_fn, LocalBoxFuture};
24use futures::stream::{self, FuturesUnordered, LocalBoxStream};
25use futures::{FutureExt, SinkExt, Stream, StreamExt};
26
27use crate::errors::{ErrorCode, H2Error};
28use crate::flow::SendWindow;
29use crate::frames::{serialize_frame, Frame, FrameDecoder, Settings, DEFAULT_MAX_FRAME_SIZE};
30use crate::hpack::{Header, HpackDecoder, HpackEncoder};
31use crate::transport::Transport;
32
33const CONNECTION_PREFACE: &[u8] = b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n";
34const SPEC_INITIAL_WINDOW: i64 = 65535;
35/// Cap on the accumulated header block (HEADERS + CONTINUATION) so an endless
36/// CONTINUATION stream can't exhaust memory (RFC 9113 §10.5.1 / CVE-2024-27316).
37const MAX_HEADER_BLOCK_SIZE: usize = 1 << 20; // 1 MiB — far above any real block
38const FORBIDDEN_HEADERS: [&str; 6] = [
39    "connection",
40    "host",
41    "keep-alive",
42    "proxy-connection",
43    "transfer-encoding",
44    "upgrade",
45];
46
47// --- public request/response types (port of types.ts) ---
48
49/// A request to issue. Missing fields default (`GET` / `/` / `http`).
50#[derive(Default)]
51pub struct RequestInit {
52    pub method: Option<String>,
53    pub path: Option<String>,
54    pub authority: Option<String>,
55    pub scheme: Option<String>,
56    pub headers: Vec<(String, String)>,
57    /// Request body. Defaults to empty; pass an in-memory buffer via `.into()`
58    /// (`Vec<u8>` / `String` / `&str`) or a chunk stream via [`RequestBody::stream`].
59    pub body: RequestBody,
60}
61
62/// A request body: nothing, an in-memory buffer, or a stream of chunks uploaded
63/// incrementally with flow control (port of the TS `BodyInit`). Build one with
64/// `RequestBody::from(..)` / `.into()` for buffers, or [`RequestBody::stream`] for
65/// any `Stream<Item = Vec<u8>>` (streaming upload).
66#[derive(Default)]
67pub enum RequestBody {
68    /// No body; the request half-closes after HEADERS.
69    #[default]
70    Empty,
71    /// A complete in-memory body, framed and flow-controlled as it is sent.
72    Bytes(Vec<u8>),
73    /// A stream of body chunks, uploaded as they arrive.
74    Stream(LocalBoxStream<'static, Vec<u8>>),
75}
76
77impl RequestBody {
78    /// Wrap any `Stream` of byte chunks as a streaming request body.
79    pub fn stream<S>(chunks: S) -> Self
80    where
81        S: Stream<Item = Vec<u8>> + 'static,
82    {
83        RequestBody::Stream(chunks.boxed_local())
84    }
85
86    /// True when there is nothing to send (so HEADERS carries END_STREAM). A
87    /// stream is assumed non-empty, matching the TS `bodyIsEmpty`.
88    fn is_empty(&self) -> bool {
89        match self {
90            RequestBody::Empty => true,
91            RequestBody::Bytes(b) => b.is_empty(),
92            RequestBody::Stream(_) => false,
93        }
94    }
95}
96
97impl From<Vec<u8>> for RequestBody {
98    fn from(v: Vec<u8>) -> Self {
99        RequestBody::Bytes(v)
100    }
101}
102impl From<&[u8]> for RequestBody {
103    fn from(v: &[u8]) -> Self {
104        RequestBody::Bytes(v.to_vec())
105    }
106}
107impl From<String> for RequestBody {
108    fn from(v: String) -> Self {
109        RequestBody::Bytes(v.into_bytes())
110    }
111}
112impl From<&str> for RequestBody {
113    fn from(v: &str) -> Self {
114        RequestBody::Bytes(v.as_bytes().to_vec())
115    }
116}
117
118/// Per-stream receive buffer, shared between the connection (which pushes DATA)
119/// and the [`ResponseBody`] (which pulls it). Bytes are held here until the
120/// consumer reads them, so an unread body applies backpressure rather than
121/// buffering unbounded (consumption-driven flow control, à la `node:http2`).
122#[derive(Default)]
123struct RecvState {
124    queue: VecDeque<Vec<u8>>,
125    /// Bytes buffered in `queue` — received but not yet returned to the receive
126    /// window (via consumption or, on drop, discard).
127    buffered: usize,
128    ended: bool,
129    error: Option<H2Error>,
130    waker: Option<Waker>,
131}
132
133/// A response body: a backpressured stream of byte-chunk results (`Err` on
134/// reset/failure, so a truncated body is never mistaken for a complete one). The
135/// connection replenishes the receive-flow window only as chunks are pulled.
136pub struct ResponseBody {
137    recv: Rc<RefCell<RecvState>>,
138    conn: Weak<RefCell<ConnState>>,
139    stream_id: u32,
140}
141
142impl ResponseBody {
143    /// Return `n` consumed/abandoned bytes to the receive windows.
144    fn replenish(&self, n: usize) {
145        if n == 0 {
146            return;
147        }
148        if let Some(conn) = self.conn.upgrade() {
149            conn.borrow().replenish_recv_window(self.stream_id, n);
150        }
151    }
152}
153
154impl Stream for ResponseBody {
155    type Item = Result<Vec<u8>, H2Error>;
156
157    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
158        let this = self.get_mut();
159        let mut recv = this.recv.borrow_mut();
160        if let Some(chunk) = recv.queue.pop_front() {
161            let n = chunk.len();
162            recv.buffered -= n;
163            drop(recv); // release before touching the connection
164            this.replenish(n); // consumption-driven WINDOW_UPDATE
165            Poll::Ready(Some(Ok(chunk)))
166        } else if let Some(e) = recv.error.take() {
167            Poll::Ready(Some(Err(e)))
168        } else if recv.ended {
169            Poll::Ready(None)
170        } else {
171            recv.waker = Some(cx.waker().clone());
172            Poll::Pending
173        }
174    }
175}
176
177impl Drop for ResponseBody {
178    fn drop(&mut self) {
179        let (remaining, still_open) = {
180            let recv = self.recv.borrow();
181            (recv.buffered, !recv.ended)
182        };
183        // Abandoned mid-stream: return the window for whatever is still buffered so
184        // the connection window doesn't leak.
185        if remaining > 0 {
186            self.replenish(remaining);
187        }
188        // ...and tell the peer to stop, because dropping the body is how a caller
189        // cancels. Without this the server keeps producing into a stream nobody will
190        // ever read: the request stays open, its handler keeps running, and a client
191        // that gave up — or whose deadline fired — silently leaks server work. RFC
192        // 7540 §8.1 calls for exactly this, and CANCEL is the code for "no longer
193        // interested" rather than an error.
194        if still_open {
195            if let Some(conn) = self.conn.upgrade() {
196                conn.borrow_mut().reset_stream(self.stream_id, ErrorCode::Cancel);
197            }
198        }
199    }
200}
201
202/// A response. The body is a stream of byte-chunk results; a chunk is `Err` if the
203/// stream was reset or the connection failed mid-download, so a truncated body is
204/// never mistaken for a complete one (mirrors the TS body stream erroring).
205pub struct Response {
206    pub status: u16,
207    pub headers: HashMap<String, String>,
208    pub raw_headers: Vec<Header>,
209    body: ResponseBody,
210    /// Trailers (a HEADERS block after the body). Shared with the stream, which
211    /// fills it in when they arrive; readable once the body has ended.
212    trailers: Rc<RefCell<Option<HashMap<String, String>>>>,
213}
214
215impl Response {
216    /// The response body as a backpressured stream of chunk results (`Err` on
217    /// reset/failure). The receive window is replenished as chunks are pulled.
218    pub fn into_body(self) -> ResponseBody {
219        self.body
220    }
221
222    /// Buffer the whole body. Errors if the stream was reset or failed mid-download.
223    pub async fn bytes(&mut self) -> Result<Vec<u8>, H2Error> {
224        let mut out = Vec::new();
225        while let Some(chunk) = self.body.next().await {
226            out.extend_from_slice(&chunk?);
227        }
228        Ok(out)
229    }
230
231    /// Buffer the body and decode it as UTF-8 (lossy).
232    pub async fn text(&mut self) -> Result<String, H2Error> {
233        Ok(String::from_utf8_lossy(&self.bytes().await?).into_owned())
234    }
235
236    /// Response trailers (a HEADERS block sent after the body), or `None` if there
237    /// were none. Read the body to completion first — trailers arrive after it.
238    pub fn trailers(&self) -> Option<HashMap<String, String>> {
239        self.trailers.borrow().clone()
240    }
241
242    /// Split into the body stream and a trailer handle that outlives it.
243    ///
244    /// [`into_body`](Self::into_body) takes `self` while [`trailers`](Self::trailers)
245    /// needs `&self`, so a caller could otherwise **stream the body or read the
246    /// trailers, never both**. gRPC needs both: its terminal status arrives in the
247    /// trailers, after the last message — so on the streaming cardinalities a caller
248    /// without this cannot tell a failed stream from a successfully empty one.
249    ///
250    /// The TypeScript client has no such split (`body` and `trailers()` are both
251    /// members of one object), so this is what keeps the two at parity.
252    pub fn into_parts(self) -> (ResponseBody, Trailers) {
253        (self.body, Trailers(self.trailers))
254    }
255}
256
257/// A handle to a response's trailers that stays readable after the body has been
258/// taken. See [`Response::into_parts`].
259#[derive(Clone)]
260pub struct Trailers(Rc<RefCell<Option<HashMap<String, String>>>>);
261
262impl Trailers {
263    /// The trailers, or `None` until the body has ended (or if there were none).
264    pub fn get(&self) -> Option<HashMap<String, String>> {
265        self.0.borrow().clone()
266    }
267}
268
269/// Settings we advertise + push handling (port of `ConnectOptions`).
270#[derive(Default, Clone)]
271pub struct ConnectOptions {
272    pub header_table_size: Option<usize>,
273    pub enable_push: Option<bool>,
274    /// Our advertised per-stream receive window (SETTINGS_INITIAL_WINDOW_SIZE).
275    /// Default 1 MiB. Replenished as the application consumes response bodies.
276    pub initial_window_size: Option<u32>,
277    pub max_frame_size: Option<usize>,
278    /// Our connection-level receive window in bytes. Default 64 MiB. Grown at
279    /// startup from the spec default of 65535 via a `WINDOW_UPDATE(0)`, then
280    /// replenished on consumption. Keep it larger than `initial_window_size` so a
281    /// single unread stream can't stall the whole connection.
282    pub connection_window_size: Option<u32>,
283    // TODO: on_push callback (pushes are currently refused).
284}
285
286// --- per-stream state (port of stream.ts) ---
287
288struct Head {
289    status: u16,
290    headers: HashMap<String, String>,
291    raw: Vec<Header>,
292}
293
294fn collect_headers(raw: Vec<Header>) -> Head {
295    let mut headers: HashMap<String, String> = HashMap::new();
296    let mut status = 0u16;
297    for h in &raw {
298        if h.name == ":status" {
299            status = h.value.parse().unwrap_or(0);
300            continue;
301        }
302        if h.name.starts_with(':') {
303            continue;
304        }
305        match headers.get(&h.name) {
306            Some(existing) => {
307                let sep = if h.name == "cookie" { "; " } else { ", " };
308                let joined = format!("{existing}{sep}{}", h.value);
309                headers.insert(h.name.clone(), joined);
310            }
311            None => {
312                headers.insert(h.name.clone(), h.value.clone());
313            }
314        }
315    }
316    Head {
317        status,
318        headers,
319        raw,
320    }
321}
322
323struct StreamState {
324    id: u32,
325    send_window: SendWindow,
326    head_tx: Option<oneshot::Sender<Result<Head, H2Error>>>,
327    /// Receive buffer shared with the `Response`'s [`ResponseBody`]. DATA is
328    /// pushed here and pulled by the consumer; the window is replenished on read.
329    recv: Rc<RefCell<RecvState>>,
330    /// Trailers cell shared with the `Response`; set when a post-body HEADERS block
331    /// (trailers) arrives.
332    trailers: Rc<RefCell<Option<HashMap<String, String>>>>,
333    got_head: bool,
334    /// Our send side is done (we sent END_STREAM: bodyless HEADERS, or the body
335    /// pump's terminal DATA). Until then the stream is at most half-closed.
336    local_closed: bool,
337    /// The peer's send side is done (we received END_STREAM). Likewise.
338    remote_closed: bool,
339}
340
341impl StreamState {
342    fn new(id: u32, initial_send_window: i64) -> Self {
343        Self {
344            id,
345            send_window: SendWindow::new(initial_send_window),
346            head_tx: None,
347            recv: Rc::new(RefCell::new(RecvState::default())),
348            trailers: Rc::new(RefCell::new(None)),
349            got_head: false,
350            local_closed: false,
351            remote_closed: false,
352        }
353    }
354
355    fn receive_headers(&mut self, raw: Vec<Header>, end_stream: bool) {
356        if !self.got_head {
357            let head = collect_headers(raw);
358            // An interim 1xx response (100 Continue, 103 Early Hints) is NOT the
359            // final response (RFC 7540 §8.1): keep waiting for the real head, and
360            // don't let a following HEADERS block be mistaken for trailers.
361            if (100..200).contains(&head.status) {
362                return;
363            }
364            self.got_head = true;
365            if let Some(tx) = self.head_tx.take() {
366                let _ = tx.send(Ok(head));
367            }
368        } else {
369            // A second HEADERS block on an open stream = trailers.
370            *self.trailers.borrow_mut() = Some(collect_headers(raw).headers);
371        }
372        if end_stream {
373            self.end_body();
374        }
375    }
376
377    fn receive_data(&mut self, data: &[u8], end_stream: bool) {
378        let mut recv = self.recv.borrow_mut();
379        if !data.is_empty() && recv.error.is_none() && !recv.ended {
380            recv.queue.push_back(data.to_vec());
381            recv.buffered += data.len();
382        }
383        if end_stream {
384            recv.ended = true;
385        }
386        if let Some(w) = recv.waker.take() {
387            w.wake();
388        }
389    }
390
391    fn receive_reset(&mut self, error_code: u32) {
392        let code = ErrorCode::from_value(error_code).unwrap_or(ErrorCode::ProtocolError);
393        self.fail(H2Error::stream(
394            code,
395            format!("stream {} reset by peer", self.id),
396            self.id,
397        ));
398    }
399
400    fn fail(&mut self, err: H2Error) {
401        self.send_window.close();
402        if !self.got_head {
403            self.got_head = true;
404            if let Some(tx) = self.head_tx.take() {
405                let _ = tx.send(Err(err.clone()));
406            }
407        }
408        // Surface the error after any already-buffered chunks (the body delivers
409        // its queue first, then this error) so a reset/failed download errors
410        // rather than looking like a clean EOF — matching the TS body.
411        let mut recv = self.recv.borrow_mut();
412        if recv.error.is_none() {
413            recv.error = Some(err);
414        }
415        // A failed stream is also a finished one. `poll_next` checks the queue, then
416        // the error, then this — so ordering is unchanged — but it means dropping the
417        // body afterwards does not mistake an already-dead stream for a live one and
418        // reset it a second time.
419        recv.ended = true;
420        if let Some(w) = recv.waker.take() {
421            w.wake();
422        }
423    }
424
425    fn end_body(&mut self) {
426        let mut recv = self.recv.borrow_mut();
427        if !recv.ended {
428            recv.ended = true;
429            if let Some(w) = recv.waker.take() {
430                w.wake();
431            }
432        }
433    }
434}
435
436// --- connection state ---
437
438struct RemoteSettings {
439    initial_window_size: i64,
440    max_frame_size: usize,
441    #[allow(dead_code)]
442    header_table_size: usize,
443    #[allow(dead_code)]
444    enable_push: bool,
445    /// Peer's SETTINGS_MAX_CONCURRENT_STREAMS — the cap on our open streams
446    /// (§5.1.2). `u32::MAX` (effectively unlimited) until the peer advertises one.
447    max_concurrent_streams: u32,
448}
449
450impl Default for RemoteSettings {
451    fn default() -> Self {
452        Self {
453            initial_window_size: SPEC_INITIAL_WINDOW,
454            max_frame_size: DEFAULT_MAX_FRAME_SIZE,
455            header_table_size: 4096,
456            enable_push: true,
457            max_concurrent_streams: u32::MAX,
458        }
459    }
460}
461
462enum HeaderBlockKind {
463    Response,
464    Push,
465}
466
467struct PendingHeaderBlock {
468    stream_id: u32,
469    kind: HeaderBlockKind,
470    end_stream: bool,
471    promised_stream_id: Option<u32>,
472    fragments: Vec<Vec<u8>>,
473    /// Running total of fragment bytes, checked against MAX_HEADER_BLOCK_SIZE.
474    size: usize,
475}
476
477/// An in-flight PING awaiting its ACK, with the send time so the round-trip time
478/// can be computed when the ACK arrives (mirrors the TS `{ resolve, sentAt }`).
479/// The channel carries a `Result` so a connection teardown can fail the waiter
480/// with the close error rather than deliver a bogus RTT.
481struct PingWaiter {
482    resolve: oneshot::Sender<Result<f64, H2Error>>,
483    sent_at: f64,
484}
485
486struct ConnState {
487    /// Outbound byte sink. `destroy` drops it so the driver's write loop drains any
488    /// still-queued frames (e.g. a GOAWAY sent during a connection error) and then
489    /// ends — the peer sees the GOAWAY before the transport closes.
490    out_tx: Option<mpsc::UnboundedSender<Vec<u8>>>,
491    /// Background body-upload pumps, run on the connection driver (see `request`).
492    task_tx: mpsc::UnboundedSender<LocalBoxFuture<'static, ()>>,
493    encoder: HpackEncoder,
494    decoder: HpackDecoder,
495    frame_decoder: FrameDecoder,
496    streams: HashMap<u32, StreamState>,
497    next_stream_id: u32,
498    conn_send_window: SendWindow,
499    remote: RemoteSettings,
500    pending_header_block: Option<PendingHeaderBlock>,
501    pings: HashMap<[u8; 8], PingWaiter>,
502    ping_counter: u32,
503    /// Requests parked waiting for a concurrent-stream slot to free up (§5.1.2).
504    slot_waiters: Vec<oneshot::Sender<()>>,
505    closed: bool,
506    close_error: Option<H2Error>,
507    goaway_received: bool,
508    highest_promised: u32,
509}
510
511impl ConnState {
512    fn write_raw(&self, bytes: Vec<u8>) {
513        if !self.closed {
514            if let Some(tx) = &self.out_tx {
515                let _ = tx.unbounded_send(bytes);
516            }
517        }
518    }
519
520    fn send_frame(&self, frame: Frame) {
521        self.write_raw(serialize_frame(&frame));
522    }
523
524    fn on_bytes(&mut self, chunk: &[u8]) {
525        let frames = match self.frame_decoder.push(chunk) {
526            Ok(f) => f,
527            Err(e) => {
528                self.connection_error(e);
529                return;
530            }
531        };
532        for frame in frames {
533            if let Err(e) = self.dispatch(frame) {
534                self.connection_error(e);
535                return;
536            }
537        }
538    }
539
540    fn dispatch(&mut self, frame: Frame) -> Result<(), H2Error> {
541        // A pending header block only allows CONTINUATION on the same stream (§6.2).
542        if self.pending_header_block.is_some() && !matches!(frame, Frame::Continuation { .. }) {
543            return Err(H2Error::new(
544                ErrorCode::ProtocolError,
545                "expected CONTINUATION frame",
546            ));
547        }
548
549        match frame {
550            Frame::Settings { ack, settings } => {
551                if ack {
552                    return Ok(());
553                }
554                self.apply_remote_settings(&settings)?;
555                self.send_frame(Frame::Settings {
556                    ack: true,
557                    settings: Settings::default(),
558                });
559            }
560            Frame::Headers {
561                stream_id,
562                header_block_fragment,
563                end_stream,
564                end_headers,
565                ..
566            } => {
567                let size = header_block_fragment.len();
568                self.pending_header_block = Some(PendingHeaderBlock {
569                    stream_id,
570                    kind: HeaderBlockKind::Response,
571                    end_stream,
572                    promised_stream_id: None,
573                    fragments: vec![header_block_fragment],
574                    size,
575                });
576                self.guard_header_block_size()?;
577                if end_headers {
578                    self.complete_header_block()?;
579                }
580            }
581            Frame::Continuation {
582                stream_id,
583                header_block_fragment,
584                end_headers,
585            } => {
586                match &mut self.pending_header_block {
587                    Some(pb) if pb.stream_id == stream_id => {
588                        pb.size += header_block_fragment.len();
589                        pb.fragments.push(header_block_fragment);
590                    }
591                    _ => {
592                        return Err(H2Error::new(
593                            ErrorCode::ProtocolError,
594                            "unexpected CONTINUATION",
595                        ))
596                    }
597                }
598                self.guard_header_block_size()?;
599                if end_headers {
600                    self.complete_header_block()?;
601                }
602            }
603            Frame::PushPromise {
604                stream_id,
605                promised_stream_id,
606                header_block_fragment,
607                end_headers,
608            } => {
609                let size = header_block_fragment.len();
610                self.pending_header_block = Some(PendingHeaderBlock {
611                    stream_id,
612                    kind: HeaderBlockKind::Push,
613                    end_stream: false,
614                    promised_stream_id: Some(promised_stream_id),
615                    fragments: vec![header_block_fragment],
616                    size,
617                });
618                self.guard_header_block_size()?;
619                if end_headers {
620                    self.complete_header_block()?;
621                }
622            }
623            Frame::Data {
624                stream_id,
625                data,
626                end_stream,
627            } => {
628                if let Some(s) = self.streams.get_mut(&stream_id) {
629                    // Buffer; the receive windows are replenished only as the app
630                    // reads the body (consumption-driven backpressure — see
631                    // ResponseBody / replenish_recv_window).
632                    s.receive_data(&data, end_stream);
633                    if end_stream {
634                        s.remote_closed = true;
635                    }
636                } else if !data.is_empty() {
637                    // DATA on an unknown/retired stream: no consumer to drive
638                    // replenishment, so return the connection window now (discarded).
639                    self.send_frame(Frame::WindowUpdate {
640                        stream_id: 0,
641                        window_size_increment: data.len() as u32,
642                    });
643                }
644                // The peer half-closing does NOT end the stream while we are still
645                // uploading — it becomes half-closed(remote) and our body pump must
646                // still be able to finish (RFC 7540 §5.1). Retire only when both
647                // directions are done.
648                if end_stream {
649                    self.retire_if_fully_closed(stream_id);
650                }
651            }
652            Frame::RstStream {
653                stream_id,
654                error_code,
655            } => {
656                if let Some(mut s) = self.streams.remove(&stream_id) {
657                    s.receive_reset(error_code);
658                }
659            }
660            Frame::WindowUpdate {
661                stream_id,
662                window_size_increment,
663            } => {
664                if window_size_increment == 0 {
665                    if stream_id == 0 {
666                        return Err(H2Error::new(ErrorCode::ProtocolError, "zero WINDOW_UPDATE"));
667                    }
668                    self.reset_stream(stream_id, ErrorCode::ProtocolError);
669                    return Ok(());
670                }
671                if stream_id == 0 {
672                    self.conn_send_window.update(window_size_increment as i64);
673                } else if let Some(s) = self.streams.get_mut(&stream_id) {
674                    s.send_window.update(window_size_increment as i64);
675                }
676            }
677            Frame::Ping { ack, opaque_data } => {
678                if ack {
679                    if let Some(w) = self.pings.remove(&opaque_data) {
680                        // Compute the RTT at ACK receipt (not when `ping` resumes).
681                        let _ = w.resolve.send(Ok(now_millis() - w.sent_at));
682                    }
683                } else {
684                    self.send_frame(Frame::Ping {
685                        ack: true,
686                        opaque_data,
687                    });
688                }
689            }
690            Frame::Goaway {
691                last_stream_id,
692                error_code,
693                ..
694            } => {
695                self.goaway_received = true;
696                let code = ErrorCode::from_value(error_code).unwrap_or(ErrorCode::NoError);
697                let err = H2Error::new(code, "peer sent GOAWAY");
698                let doomed: Vec<u32> = self
699                    .streams
700                    .keys()
701                    .copied()
702                    .filter(|&id| id > last_stream_id)
703                    .collect();
704                for id in doomed {
705                    if let Some(mut s) = self.streams.remove(&id) {
706                        s.fail(err.clone());
707                    }
708                }
709                self.wake_slot_waiters(); // parked requests now reject (going away)
710                if error_code != 0 {
711                    self.destroy(err);
712                }
713            }
714            Frame::Priority { .. } => {} // prioritization not implemented
715        }
716        Ok(())
717    }
718
719    /// Bound the accumulated header block so an endless CONTINUATION stream can't
720    /// exhaust memory (RFC 9113 §10.5.1 / CVE-2024-27316).
721    fn guard_header_block_size(&self) -> Result<(), H2Error> {
722        if let Some(pb) = &self.pending_header_block {
723            if pb.size > MAX_HEADER_BLOCK_SIZE {
724                return Err(H2Error::new(
725                    ErrorCode::EnhanceYourCalm,
726                    "header block exceeds the maximum size",
727                ));
728            }
729        }
730        Ok(())
731    }
732
733    fn complete_header_block(&mut self) -> Result<(), H2Error> {
734        let pb = self
735            .pending_header_block
736            .take()
737            .expect("header block present");
738        let block: Vec<u8> = if pb.fragments.len() == 1 {
739            pb.fragments.into_iter().next().unwrap()
740        } else {
741            pb.fragments.concat()
742        };
743        let headers = self.decoder.decode(&block)?;
744
745        match pb.kind {
746            HeaderBlockKind::Response => {
747                if let Some(s) = self.streams.get_mut(&pb.stream_id) {
748                    s.receive_headers(headers, pb.end_stream);
749                    if pb.end_stream {
750                        s.remote_closed = true;
751                    }
752                }
753                if pb.end_stream {
754                    self.retire_if_fully_closed(pb.stream_id);
755                }
756            }
757            HeaderBlockKind::Push => {
758                let promised = pb.promised_stream_id.unwrap_or(0);
759                if promised > self.highest_promised {
760                    self.highest_promised = promised;
761                }
762                // TODO: surface pushes via an on_push callback. For now, refuse.
763                self.send_frame(Frame::RstStream {
764                    stream_id: promised,
765                    error_code: ErrorCode::RefusedStream.value(),
766                });
767            }
768        }
769        Ok(())
770    }
771
772    fn apply_remote_settings(&mut self, s: &Settings) -> Result<(), H2Error> {
773        if let Some(iw) = s.initial_window_size {
774            // §6.5.2: a window above 2^31-1 is a FLOW_CONTROL_ERROR.
775            if iw > 0x7fff_ffff {
776                return Err(H2Error::new(
777                    ErrorCode::FlowControlError,
778                    "SETTINGS_INITIAL_WINDOW_SIZE exceeds 2^31-1",
779                ));
780            }
781            let delta = iw as i64 - self.remote.initial_window_size;
782            self.remote.initial_window_size = iw as i64;
783            for stream in self.streams.values_mut() {
784                stream.send_window.adjust(delta);
785            }
786        }
787        if let Some(mfs) = s.max_frame_size {
788            // §6.5.2: MAX_FRAME_SIZE must be within 2^14..2^24-1.
789            if !(16384..=16_777_215).contains(&mfs) {
790                return Err(H2Error::new(
791                    ErrorCode::ProtocolError,
792                    "SETTINGS_MAX_FRAME_SIZE out of range",
793                ));
794            }
795            self.remote.max_frame_size = mfs as usize;
796        }
797        if let Some(hts) = s.header_table_size {
798            self.remote.header_table_size = hts as usize;
799        }
800        if let Some(ep) = s.enable_push {
801            self.remote.enable_push = ep;
802        }
803        if let Some(mcs) = s.max_concurrent_streams {
804            self.remote.max_concurrent_streams = mcs;
805            self.wake_slot_waiters(); // a raised limit may free parked requests
806        }
807        Ok(())
808    }
809
810    fn send_headers(&self, id: u32, block: Vec<u8>, end_stream: bool) {
811        let max = self.remote.max_frame_size;
812        if block.len() <= max {
813            self.send_frame(Frame::Headers {
814                stream_id: id,
815                header_block_fragment: block,
816                end_stream,
817                end_headers: true,
818                priority: None,
819            });
820            return;
821        }
822        // Split an oversized block into HEADERS + CONTINUATION frames.
823        self.send_frame(Frame::Headers {
824            stream_id: id,
825            header_block_fragment: block[..max].to_vec(),
826            end_stream,
827            end_headers: false,
828            priority: None,
829        });
830        let mut offset = max;
831        while offset < block.len() {
832            let next = (offset + max).min(block.len());
833            self.send_frame(Frame::Continuation {
834                stream_id: id,
835                header_block_fragment: block[offset..next].to_vec(),
836                end_headers: next >= block.len(),
837            });
838            offset = next;
839        }
840    }
841
842    fn reset_stream(&mut self, id: u32, code: ErrorCode) {
843        self.send_frame(Frame::RstStream {
844            stream_id: id,
845            error_code: code.value(),
846        });
847        // Fail the pending request/body with a proper error rather than relying on
848        // the dropped head sender surfacing a generic "connection closed".
849        if let Some(mut s) = self.streams.remove(&id) {
850            s.fail(H2Error::stream(code, format!("stream {id} reset"), id));
851        }
852        self.wake_slot_waiters();
853    }
854
855    /// Drop a stream only once BOTH directions have ended. A one-sided close
856    /// (the peer's END_STREAM while we are still uploading, or our upload
857    /// finishing before the peer replies) leaves it half-closed and in the map,
858    /// so the still-open direction — and any WINDOW_UPDATEs for it — keep working.
859    fn retire_if_fully_closed(&mut self, id: u32) {
860        let fully_closed = self
861            .streams
862            .get(&id)
863            .is_some_and(|s| s.local_closed && s.remote_closed);
864        if fully_closed {
865            self.streams.remove(&id);
866            self.wake_slot_waiters(); // a freed slot may admit a parked request
867        }
868    }
869
870    /// Return `n` bytes to our receive-flow windows once the application has
871    /// consumed them from a response body (consumption-driven flow control). The
872    /// stream window is replenished only while the stream is still open; the
873    /// connection window always is (so an abandoned/retired stream can't leak it).
874    fn replenish_recv_window(&self, stream_id: u32, n: usize) {
875        if self.closed || n == 0 {
876            return;
877        }
878        let inc = n as u32;
879        if self.streams.contains_key(&stream_id) {
880            self.send_frame(Frame::WindowUpdate {
881                stream_id,
882                window_size_increment: inc,
883            });
884        }
885        self.send_frame(Frame::WindowUpdate {
886            stream_id: 0,
887            window_size_increment: inc,
888        });
889    }
890
891    // --- concurrent-stream limiting (§5.1.2) ---
892
893    /// Client-initiated (odd-id) streams currently open or half-closed — the ones
894    /// that count toward the peer's SETTINGS_MAX_CONCURRENT_STREAMS.
895    fn active_streams(&self) -> usize {
896        self.streams.keys().filter(|id| *id % 2 == 1).count()
897    }
898
899    /// True if opening another request stream would stay within the peer's limit.
900    fn can_open_stream(&self) -> bool {
901        self.active_streams() < self.remote.max_concurrent_streams as usize
902    }
903
904    /// Wake every parked request so it re-checks for a free slot (or a teardown).
905    fn wake_slot_waiters(&mut self) {
906        for tx in self.slot_waiters.drain(..) {
907            let _ = tx.send(());
908        }
909    }
910
911    fn connection_error(&mut self, err: H2Error) {
912        self.send_frame(Frame::Goaway {
913            last_stream_id: self.highest_promised,
914            error_code: err.code.value(),
915            debug_data: Vec::new(),
916        });
917        self.destroy(err);
918    }
919
920    fn destroy(&mut self, err: H2Error) {
921        if self.closed {
922            return;
923        }
924        self.closed = true;
925        self.close_error = Some(err.clone());
926        self.conn_send_window.close();
927        let ids: Vec<u32> = self.streams.keys().copied().collect();
928        for id in ids {
929            if let Some(mut s) = self.streams.remove(&id) {
930                s.fail(err.clone());
931            }
932        }
933        // Fail every in-flight ping with the close error (mirrors the TS reject).
934        for (_, w) in self.pings.drain() {
935            let _ = w.resolve.send(Err(err.clone()));
936        }
937        self.wake_slot_waiters(); // parked requests wake and see the closed state
938        // Drop the outbound sender: the write loop drains whatever is still queued
939        // (a GOAWAY from `connection_error`/`close`, say) and then ends.
940        self.out_tx = None;
941    }
942}
943
944/// The HTTP/2 connection handle. Cheap to clone (shares one `Rc` state).
945#[derive(Clone)]
946pub struct H2Connection {
947    shared: Rc<RefCell<ConnState>>,
948}
949
950impl H2Connection {
951    /// True once the connection has been torn down.
952    pub fn is_closed(&self) -> bool {
953        self.shared.borrow().closed
954    }
955
956    /// Client-initiated streams currently open or half-closed — the ones that
957    /// count toward the peer's SETTINGS_MAX_CONCURRENT_STREAMS (§5.1.2).
958    pub fn active_streams(&self) -> usize {
959        self.shared.borrow().active_streams()
960    }
961
962    /// True if a new request would stay within the peer's advertised
963    /// SETTINGS_MAX_CONCURRENT_STREAMS. A connection pool uses this to decide
964    /// whether to route here or open a fresh connection; a direct caller need not
965    /// check — [`request`](Self::request) parks until a slot frees.
966    pub fn can_open_stream(&self) -> bool {
967        self.shared.borrow().can_open_stream()
968    }
969
970    /// The negotiated WebSocket subprotocol, if opened via a WebSocket (set by
971    /// the caller). Empty otherwise.
972    // (Kept minimal here; `connect_websocket` sets it in the web layer.)
973    pub async fn request(&self, mut init: RequestInit) -> Result<Response, H2Error> {
974        let body = std::mem::take(&mut init.body);
975        let has_body = !body.is_empty();
976
977        // Respect the peer's SETTINGS_MAX_CONCURRENT_STREAMS: park until a slot
978        // frees (§5.1.2). There is no await between the passing check and the
979        // synchronous reservation below, so woken waiters can't over-allocate.
980        loop {
981            let rx = {
982                let mut st = self.shared.borrow_mut();
983                if st.closed {
984                    return Err(st.close_error.clone().unwrap_or_else(|| {
985                        H2Error::new(ErrorCode::InternalError, "connection closed")
986                    }));
987                }
988                if st.goaway_received {
989                    return Err(H2Error::new(
990                        ErrorCode::RefusedStream,
991                        "connection is going away",
992                    ));
993                }
994                if st.can_open_stream() {
995                    None
996                } else {
997                    let (tx, rx) = oneshot::channel();
998                    st.slot_waiters.push(tx);
999                    Some(rx)
1000                }
1001            };
1002            match rx {
1003                None => break,
1004                Some(rx) => {
1005                    let _ = rx.await;
1006                }
1007            }
1008        }
1009
1010        let id;
1011        let head_rx;
1012        let recv;
1013        let task_tx;
1014        let trailers;
1015        {
1016            let mut st = self.shared.borrow_mut();
1017            if st.closed {
1018                return Err(st.close_error.clone().unwrap_or_else(|| {
1019                    H2Error::new(ErrorCode::InternalError, "connection closed")
1020                }));
1021            }
1022            if st.goaway_received {
1023                return Err(H2Error::new(
1024                    ErrorCode::RefusedStream,
1025                    "connection is going away",
1026                ));
1027            }
1028
1029            id = st.next_stream_id;
1030            st.next_stream_id += 2;
1031
1032            let (htx, hrx) = oneshot::channel();
1033            let initial = st.remote.initial_window_size;
1034            let mut stream = StreamState::new(id, initial);
1035            stream.head_tx = Some(htx);
1036            // A bodyless request half-closes immediately (HEADERS carries END_STREAM).
1037            stream.local_closed = !has_body;
1038            trailers = stream.trailers.clone();
1039            recv = stream.recv.clone(); // shared with the ResponseBody built below
1040            st.streams.insert(id, stream);
1041            head_rx = hrx;
1042
1043            let headers = build_request_headers(&init);
1044            let block = st.encoder.encode(&headers);
1045            st.send_headers(id, block, !has_body);
1046            task_tx = st.task_tx.clone();
1047        }
1048
1049        // Upload the body concurrently: the pump runs on the connection driver, so
1050        // the response head (and even response body) can arrive while we are still
1051        // sending — true bidirectional streaming. No-op for bodyless requests.
1052        if has_body {
1053            let pump = pump_body(self.shared.clone(), id, body);
1054            let _ = task_tx.unbounded_send(pump.boxed_local());
1055        }
1056
1057        match head_rx.await {
1058            Ok(Ok(head)) => Ok(Response {
1059                status: head.status,
1060                headers: head.headers,
1061                raw_headers: head.raw,
1062                body: ResponseBody {
1063                    recv,
1064                    conn: Rc::downgrade(&self.shared),
1065                    stream_id: id,
1066                },
1067                trailers,
1068            }),
1069            Ok(Err(e)) => Err(e),
1070            Err(_canceled) => Err(self
1071                .shared
1072                .borrow()
1073                .close_error
1074                .clone()
1075                .unwrap_or_else(|| H2Error::new(ErrorCode::InternalError, "connection closed"))),
1076        }
1077    }
1078
1079    /// Send a PING and resolve with the round-trip time in milliseconds.
1080    pub async fn ping(&self) -> Result<f64, H2Error> {
1081        let rx = {
1082            let mut st = self.shared.borrow_mut();
1083            if st.closed {
1084                return Err(st.close_error.clone().unwrap_or_else(|| {
1085                    H2Error::new(ErrorCode::InternalError, "connection closed")
1086                }));
1087            }
1088            st.ping_counter = st.ping_counter.wrapping_add(1);
1089            let mut opaque = [0u8; 8];
1090            opaque[4..8].copy_from_slice(&st.ping_counter.to_be_bytes());
1091            let (tx, rx) = oneshot::channel();
1092            st.pings.insert(
1093                opaque,
1094                PingWaiter {
1095                    resolve: tx,
1096                    sent_at: now_millis(),
1097                },
1098            );
1099            st.send_frame(Frame::Ping {
1100                ack: false,
1101                opaque_data: opaque,
1102            });
1103            rx
1104        };
1105        // `Ok(res)` carries the ACK RTT or the teardown error; a bare `Canceled`
1106        // (sender dropped without a value) collapses to a generic closed error.
1107        match rx.await {
1108            Ok(res) => res,
1109            Err(_canceled) => Err(H2Error::new(ErrorCode::InternalError, "connection closed")),
1110        }
1111    }
1112
1113    /// Gracefully close: send GOAWAY, then tear down.
1114    pub fn close(&self) {
1115        let mut st = self.shared.borrow_mut();
1116        if st.closed {
1117            return;
1118        }
1119        st.send_frame(Frame::Goaway {
1120            last_stream_id: st.highest_promised,
1121            error_code: 0,
1122            debug_data: Vec::new(),
1123        });
1124        st.destroy(H2Error::new(
1125            ErrorCode::NoError,
1126            "connection closed by client",
1127        ));
1128    }
1129}
1130
1131/// Upload a request body chunk-by-chunk, honoring connection- and stream-level
1132/// flow control, then half-close the stream with an empty END_STREAM DATA frame.
1133/// Runs on the connection driver (registered by `request`), so it does not block
1134/// the caller — the response may stream back while this is still uploading.
1135async fn pump_body(shared: Rc<RefCell<ConnState>>, id: u32, body: RequestBody) {
1136    let mut chunks: LocalBoxStream<'static, Vec<u8>> = match body {
1137        RequestBody::Empty => return,
1138        RequestBody::Bytes(bytes) => stream::once(async move { bytes }).boxed_local(),
1139        RequestBody::Stream(s) => s,
1140    };
1141    while let Some(chunk) = chunks.next().await {
1142        if chunk.is_empty() {
1143            continue;
1144        }
1145        if !pump_chunk(&shared, id, &chunk).await {
1146            return; // stream reset / connection closed mid-upload; no END_STREAM
1147        }
1148    }
1149    let mut st = shared.borrow_mut();
1150    // The stream may have been reset/torn down while we uploaded the last chunk;
1151    // only half-close a stream that is still live.
1152    if st.streams.contains_key(&id) {
1153        st.send_frame(Frame::Data {
1154            stream_id: id,
1155            data: Vec::new(),
1156            end_stream: true,
1157        });
1158        if let Some(s) = st.streams.get_mut(&id) {
1159            s.local_closed = true;
1160        }
1161        // If the peer already sent its END_STREAM, both sides are now done.
1162        st.retire_if_fully_closed(id);
1163    }
1164}
1165
1166/// Send one body chunk as DATA frames, awaiting positive connection- and
1167/// stream-level send windows between frames. Returns `false` if the stream or
1168/// connection tore down mid-chunk (so the caller must not send END_STREAM).
1169async fn pump_chunk(shared: &Rc<RefCell<ConnState>>, id: u32, chunk: &[u8]) -> bool {
1170    let mut offset = 0;
1171    while offset < chunk.len() {
1172        // Await positive connection- and stream-level send windows.
1173        let alive = poll_fn(|cx| {
1174            let mut st = shared.borrow_mut();
1175            if st.closed || !st.streams.contains_key(&id) {
1176                return Poll::Ready(false);
1177            }
1178            let conn_ready = st.conn_send_window.is_ready();
1179            let stream_ready = st
1180                .streams
1181                .get(&id)
1182                .map(|s| s.send_window.is_ready())
1183                .unwrap_or(false);
1184            if conn_ready && stream_ready {
1185                Poll::Ready(true)
1186            } else {
1187                if !conn_ready {
1188                    st.conn_send_window.register_waker(cx.waker());
1189                }
1190                if !stream_ready {
1191                    if let Some(s) = st.streams.get_mut(&id) {
1192                        s.send_window.register_waker(cx.waker());
1193                    }
1194                }
1195                Poll::Pending
1196            }
1197        })
1198        .await;
1199        if !alive {
1200            return false;
1201        }
1202
1203        let mut st = shared.borrow_mut();
1204        if st.closed
1205            || st
1206                .streams
1207                .get(&id)
1208                .map(|s| s.send_window.is_closed())
1209                .unwrap_or(true)
1210        {
1211            return false;
1212        }
1213        let conn_w = st.conn_send_window.value();
1214        let stream_w = st.streams.get(&id).unwrap().send_window.value();
1215        let max = st.remote.max_frame_size as i64;
1216        let remaining = (chunk.len() - offset) as i64;
1217        let grant = remaining.min(conn_w).min(stream_w).min(max);
1218        if grant <= 0 {
1219            continue; // windows changed under us; re-await
1220        }
1221        st.conn_send_window.consume(grant);
1222        st.streams.get_mut(&id).unwrap().send_window.consume(grant);
1223        let slice = chunk[offset..offset + grant as usize].to_vec();
1224        st.send_frame(Frame::Data {
1225            stream_id: id,
1226            data: slice,
1227            end_stream: false,
1228        });
1229        offset += grant as usize;
1230    }
1231    true
1232}
1233
1234/// Current time in milliseconds, for PING round-trip timing. On `wasm32` this is
1235/// the browser clock via `js_sys::Date::now()` — the Rust binding for JS
1236/// `Date.now()` (what the TS client uses); it needs no `window`, so it also works
1237/// in Web Workers. Off-wasm (host tests) it falls back to the system clock.
1238#[cfg(target_arch = "wasm32")]
1239fn now_millis() -> f64 {
1240    js_sys::Date::now()
1241}
1242
1243#[cfg(not(target_arch = "wasm32"))]
1244fn now_millis() -> f64 {
1245    use std::time::{SystemTime, UNIX_EPOCH};
1246    SystemTime::now()
1247        .duration_since(UNIX_EPOCH)
1248        .map(|d| d.as_secs_f64() * 1000.0)
1249        .unwrap_or(0.0)
1250}
1251
1252fn build_request_headers(init: &RequestInit) -> Vec<Header> {
1253    let method = init
1254        .method
1255        .clone()
1256        .unwrap_or_else(|| "GET".into())
1257        .to_uppercase();
1258    let scheme = init.scheme.clone().unwrap_or_else(|| "http".into());
1259    let path = init.path.clone().unwrap_or_else(|| "/".into());
1260
1261    let mut headers = vec![
1262        Header::new(":method", method),
1263        Header::new(":scheme", scheme),
1264    ];
1265    if let Some(auth) = &init.authority {
1266        headers.push(Header::new(":authority", auth.clone()));
1267    }
1268    headers.push(Header::new(":path", path));
1269
1270    for (raw_name, value) in &init.headers {
1271        let name = raw_name.to_ascii_lowercase();
1272        if name.starts_with(':') || FORBIDDEN_HEADERS.contains(&name.as_str()) {
1273            continue;
1274        }
1275        if name == "authorization" || name == "cookie" {
1276            headers.push(Header::never_indexed(name, value.clone()));
1277        } else {
1278            headers.push(Header::new(name, value.clone()));
1279        }
1280    }
1281    headers
1282}
1283
1284/// Create an HTTP/2 client over a byte [`Transport`], speaking prior knowledge.
1285///
1286/// Returns the connection handle plus a **driver** future that runs the read and
1287/// write loops; the caller must spawn/poll it (on wasm, `spawn_local`). The
1288/// preface + SETTINGS are queued immediately, so [`H2Connection::request`] may be
1289/// called right away.
1290pub fn connect(
1291    transport: Transport,
1292    options: ConnectOptions,
1293) -> (H2Connection, impl Future<Output = ()>) {
1294    let (out_tx, out_rx) = mpsc::unbounded();
1295    let (task_tx, task_rx) = mpsc::unbounded();
1296
1297    let local_max_frame_size = options.max_frame_size.unwrap_or(DEFAULT_MAX_FRAME_SIZE);
1298    let local_initial_window = options.initial_window_size.unwrap_or(1024 * 1024);
1299    let conn_recv_window = options.connection_window_size.unwrap_or(64 * 1024 * 1024);
1300    let header_table_size = options.header_table_size.unwrap_or(4096);
1301    let enable_push = options.enable_push.unwrap_or(true);
1302
1303    let state = ConnState {
1304        out_tx: Some(out_tx),
1305        task_tx,
1306        encoder: HpackEncoder::new(),
1307        decoder: HpackDecoder::new(header_table_size),
1308        frame_decoder: FrameDecoder::new(local_max_frame_size),
1309        streams: HashMap::new(),
1310        next_stream_id: 1,
1311        conn_send_window: SendWindow::new(SPEC_INITIAL_WINDOW),
1312        remote: RemoteSettings::default(),
1313        pending_header_block: None,
1314        pings: HashMap::new(),
1315        ping_counter: 0,
1316        slot_waiters: Vec::new(),
1317        closed: false,
1318        close_error: None,
1319        goaway_received: false,
1320        highest_promised: 0,
1321    };
1322    let shared = Rc::new(RefCell::new(state));
1323
1324    // Client connection preface + our SETTINGS, sent immediately (§3.5).
1325    {
1326        let st = shared.borrow();
1327        st.write_raw(CONNECTION_PREFACE.to_vec());
1328        st.send_frame(Frame::Settings {
1329            ack: false,
1330            settings: Settings {
1331                header_table_size: Some(header_table_size as u32),
1332                enable_push: Some(enable_push),
1333                initial_window_size: Some(local_initial_window),
1334                max_frame_size: Some(local_max_frame_size as u32),
1335                ..Default::default()
1336            },
1337        });
1338        // Grow the connection-level receive window past the spec default of 65535
1339        // (§6.9.2). Thereafter it — like each stream window — is replenished only
1340        // as the application consumes response bodies (consumption-driven).
1341        let grow = conn_recv_window as i64 - SPEC_INITIAL_WINDOW;
1342        if grow > 0 {
1343            st.send_frame(Frame::WindowUpdate {
1344                stream_id: 0,
1345                window_size_increment: grow as u32,
1346            });
1347        }
1348    }
1349
1350    let driver = drive(
1351        shared.clone(),
1352        transport.reader,
1353        transport.writer,
1354        out_rx,
1355        task_rx,
1356    );
1357    (H2Connection { shared }, driver)
1358}
1359
1360async fn drive(
1361    shared: Rc<RefCell<ConnState>>,
1362    mut reader: crate::transport::ByteStream,
1363    mut writer: crate::transport::ByteSink,
1364    mut out_rx: mpsc::UnboundedReceiver<Vec<u8>>,
1365    task_rx: mpsc::UnboundedReceiver<LocalBoxFuture<'static, ()>>,
1366) {
1367    let read = {
1368        let shared = shared.clone();
1369        async move {
1370            while let Some(chunk) = reader.next().await {
1371                if !chunk.is_empty() {
1372                    shared.borrow_mut().on_bytes(&chunk);
1373                }
1374                if shared.borrow().closed {
1375                    break;
1376                }
1377            }
1378            shared
1379                .borrow_mut()
1380                .destroy(H2Error::new(ErrorCode::NoError, "transport closed by peer"));
1381        }
1382    };
1383
1384    let write = async move {
1385        while let Some(bytes) = out_rx.next().await {
1386            if writer.send(bytes).await.is_err() {
1387                shared.borrow_mut().destroy(H2Error::new(
1388                    ErrorCode::InternalError,
1389                    "transport write failed",
1390                ));
1391                break;
1392            }
1393        }
1394    };
1395
1396    // Body-upload pumps registered by `request` run here too, so uploads proceed
1397    // concurrently with the read/write loops (and with one another).
1398    let tasks = run_tasks(task_rx);
1399
1400    // The write loop defines the driver's lifetime: it flushes every queued frame —
1401    // including a GOAWAY queued during teardown — and ends only once `destroy` has
1402    // dropped the outbound sender. read + tasks run alongside to process inbound
1403    // frames and body uploads (and to trigger teardown); their completion alone does
1404    // not end the driver, so the final flush is never cut short.
1405    let read = read.fuse();
1406    let tasks = tasks.fuse();
1407    let write = write.fuse();
1408    futures::pin_mut!(read, write, tasks);
1409    futures::future::poll_fn(|cx| {
1410        let _ = read.as_mut().poll(cx);
1411        let _ = tasks.as_mut().poll(cx);
1412        write.as_mut().poll(cx)
1413    })
1414    .await;
1415}
1416
1417/// Drive registered background tasks (body-upload pumps) to completion. Never
1418/// resolves on its own; it is dropped when the read/write loop ends (teardown).
1419async fn run_tasks(mut task_rx: mpsc::UnboundedReceiver<LocalBoxFuture<'static, ()>>) {
1420    let mut pending: FuturesUnordered<LocalBoxFuture<'static, ()>> = FuturesUnordered::new();
1421    poll_fn(move |cx| {
1422        // Absorb any newly-registered pumps.
1423        while let Poll::Ready(Some(task)) = task_rx.poll_next_unpin(cx) {
1424            pending.push(task);
1425        }
1426        // Advance in-flight pumps; drop completions (empties fall through to Pending).
1427        while let Poll::Ready(Some(())) = pending.poll_next_unpin(cx) {}
1428        Poll::Pending
1429    })
1430    .await
1431}