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