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