Skip to main content

ringline_http/
h2_conn.rs

1//! Async HTTP/2 connection wrapping `H2Connection` with a pump loop.
2//!
3//! Follows the momento pattern: fire requests synchronously, then pump the
4//! connection (recv bytes → feed H2 → dispatch events → flush sends) until
5//! a stream completes.
6
7use std::collections::{HashMap, VecDeque};
8use std::net::SocketAddr;
9
10use bytes::{Bytes, BytesMut};
11use ringline::{ConnCtx, ParseResult};
12use ringline_h2::hpack::HeaderField;
13use ringline_h2::settings::Settings;
14use ringline_h2::{H2Connection, H2Event};
15
16use crate::error::HttpError;
17use crate::response::Response;
18
19/// State of a pending HTTP/2 stream.
20struct PendingStream {
21    status: Option<u16>,
22    headers: Vec<(String, String)>,
23    body: BytesMut,
24    done: bool,
25    /// When true, DATA payloads are pushed to `chunks` instead of `body`.
26    streaming: bool,
27    /// Buffered chunks for streaming responses.
28    chunks: VecDeque<Bytes>,
29    /// Content-Encoding from response headers (for decompression).
30    content_encoding: Option<String>,
31}
32
33impl PendingStream {
34    fn new() -> Self {
35        Self {
36            status: None,
37            headers: Vec::new(),
38            body: BytesMut::new(),
39            done: false,
40            streaming: false,
41            chunks: VecDeque::new(),
42            content_encoding: None,
43        }
44    }
45
46    fn into_response(self) -> Result<Response, HttpError> {
47        let body = self.body.freeze();
48
49        // Decompress body if Content-Encoding is set.
50        #[cfg(any(feature = "gzip", feature = "zstd", feature = "brotli"))]
51        if let Some(ref encoding) = self.content_encoding {
52            let decompressed = crate::compress::decompress(encoding, &body)?;
53            return Ok(Response::new(
54                self.status.unwrap_or(0),
55                self.headers,
56                Bytes::from(decompressed),
57            ));
58        }
59
60        Ok(Response::new(self.status.unwrap_or(0), self.headers, body))
61    }
62}
63
64/// Data for a send blocked on flow control.
65struct BlockedSend {
66    stream_id: u32,
67    data: Vec<u8>,
68    end_stream: bool,
69}
70
71/// Async HTTP/2 connection with multiplexed request support.
72///
73/// Wraps a sans-IO `H2Connection` and a `ConnCtx`, providing a pump loop
74/// that bridges bytes between the transport and the H2 state machine.
75pub struct H2AsyncConn {
76    conn: ConnCtx,
77    h2: H2Connection,
78    pending_streams: HashMap<u32, PendingStream>,
79    blocked_sends: VecDeque<BlockedSend>,
80    /// Streams that completed during a pump cycle, ready for pickup.
81    completed: VecDeque<(u32, Response)>,
82    settings_acked: bool,
83}
84
85impl H2AsyncConn {
86    /// Connect to an HTTP/2 server over TLS.
87    ///
88    /// Performs TLS handshake, sends the H2 connection preface, and waits
89    /// for the server SETTINGS exchange to complete.
90    pub async fn connect(addr: SocketAddr, host: &str) -> Result<Self, HttpError> {
91        let conn = ringline::connect_tls(addr, host)?.await?;
92        Self::from_conn(conn).await
93    }
94
95    /// Connect with a timeout (milliseconds).
96    pub async fn connect_with_timeout(
97        addr: SocketAddr,
98        host: &str,
99        timeout_ms: u64,
100    ) -> Result<Self, HttpError> {
101        let conn = ringline::connect_tls_with_timeout(addr, host, timeout_ms)?.await?;
102        Self::from_conn(conn).await
103    }
104
105    /// Wrap an already-connected `ConnCtx` (must be TLS for H2).
106    ///
107    /// Sends the H2 preface and waits for SETTINGS exchange.
108    pub async fn from_conn(conn: ConnCtx) -> Result<Self, HttpError> {
109        let h2 = H2Connection::new(Settings::client_default());
110
111        let mut this = Self {
112            conn,
113            h2,
114            pending_streams: HashMap::new(),
115            blocked_sends: VecDeque::new(),
116            completed: VecDeque::new(),
117            settings_acked: false,
118        };
119
120        // Send the connection preface (magic + SETTINGS).
121        this.flush_pending_send()?;
122
123        // Pump until we get SettingsAcknowledged.
124        while !this.settings_acked {
125            this.pump_once().await?;
126        }
127
128        Ok(this)
129    }
130
131    /// Returns the underlying connection context.
132    pub fn close(&self) {
133        self.conn.close();
134    }
135
136    pub fn conn(&self) -> ConnCtx {
137        self.conn
138    }
139
140    /// Number of in-flight streams.
141    pub fn pending_count(&self) -> usize {
142        self.pending_streams.len()
143    }
144
145    // ── Sequential API ─────────────────────────────────────────────────
146
147    /// Send a request and wait for the complete response.
148    pub async fn send_request(
149        &mut self,
150        method: &str,
151        path: &str,
152        host: &str,
153        extra_headers: &[(&str, &str)],
154        body: Option<&[u8]>,
155    ) -> Result<Response, HttpError> {
156        let stream_id = self.fire_request(method, path, host, extra_headers, body)?;
157        self.recv_stream(stream_id).await
158    }
159
160    // ── Multiplexed fire API ───────────────────────────────────────────
161
162    /// Fire an HTTP/2 request. Returns the stream ID immediately.
163    ///
164    /// The request is queued for sending; call `recv()` or `recv_stream()`
165    /// to pump the connection and collect responses.
166    pub fn fire_request(
167        &mut self,
168        method: &str,
169        path: &str,
170        host: &str,
171        extra_headers: &[(&str, &str)],
172        body: Option<&[u8]>,
173    ) -> Result<u32, HttpError> {
174        let has_body = body.is_some_and(|b| !b.is_empty());
175
176        let mut headers = vec![
177            HeaderField::new(b":method", method.as_bytes()),
178            HeaderField::new(b":path", path.as_bytes()),
179            HeaderField::new(b":scheme", b"https"),
180            HeaderField::new(b":authority", host.as_bytes()),
181        ];
182
183        let has_accept_encoding = extra_headers
184            .iter()
185            .any(|(name, _)| name.eq_ignore_ascii_case("accept-encoding"));
186
187        for (name, value) in extra_headers {
188            headers.push(HeaderField::new(name.as_bytes(), value.as_bytes()));
189        }
190
191        // Auto-inject Accept-Encoding when compression features are enabled
192        // and the caller has not already set one.
193        if !has_accept_encoding && let Some(ae) = crate::compress::accept_encoding_value() {
194            headers.push(HeaderField::new(b"accept-encoding", ae.as_bytes()));
195        }
196
197        let end_stream = !has_body;
198        let stream_id = self.h2.send_request(&headers, end_stream)?;
199
200        if let Some(data) = body
201            && !data.is_empty()
202            && let Err(e) = self.h2.send_data(stream_id, data, true)
203        {
204            if matches!(e, ringline_h2::H2Error::FlowControlError) {
205                self.blocked_sends.push_back(BlockedSend {
206                    stream_id,
207                    data: data.to_vec(),
208                    end_stream: true,
209                });
210            } else {
211                return Err(HttpError::H2(e));
212            }
213        }
214
215        self.pending_streams.insert(stream_id, PendingStream::new());
216
217        // Flush the request frames to the transport.
218        self.flush_pending_send()?;
219
220        Ok(stream_id)
221    }
222
223    // ── Multiplexed recv API ───────────────────────────────────────────
224
225    /// Pump until any stream completes. Returns `(stream_id, Response)`.
226    pub async fn recv(&mut self) -> Result<(u32, Response), HttpError> {
227        // Check if we already have a completed response queued.
228        if let Some(completed) = self.completed.pop_front() {
229            return Ok(completed);
230        }
231
232        loop {
233            self.pump_once().await?;
234
235            if let Some(completed) = self.completed.pop_front() {
236                return Ok(completed);
237            }
238        }
239    }
240
241    /// Pump until a specific stream completes.
242    pub async fn recv_stream(&mut self, stream_id: u32) -> Result<Response, HttpError> {
243        // Check completed queue first.
244        if let Some(idx) = self.completed.iter().position(|(sid, _)| *sid == stream_id) {
245            let (_, resp) = self.completed.remove(idx).unwrap();
246            return Ok(resp);
247        }
248
249        loop {
250            self.pump_once().await?;
251
252            // Check if our target stream completed.
253            if let Some(idx) = self.completed.iter().position(|(sid, _)| *sid == stream_id) {
254                let (_, resp) = self.completed.remove(idx).unwrap();
255                return Ok(resp);
256            }
257        }
258    }
259
260    // ── Internal pump loop ─────────────────────────────────────────────
261
262    /// One round of the pump loop:
263    /// 1. Flush pending H2 output to transport
264    /// 2. Read bytes from transport, feed to H2
265    /// 3. Dispatch H2 events to pending streams
266    /// 4. Retry blocked sends (flow control may have opened)
267    /// 5. Flush any protocol responses (WINDOW_UPDATE, PING ACK, etc.)
268    async fn pump_once(&mut self) -> Result<(), HttpError> {
269        // Flush any pending output before blocking on recv.
270        self.flush_pending_send()?;
271
272        // Borrow-split: capture mutable refs before the closure.
273        let h2 = &mut self.h2;
274        let pending = &mut self.pending_streams;
275        let blocked = &mut self.blocked_sends;
276        let settings_acked = &mut self.settings_acked;
277
278        let n = self
279            .conn
280            .with_data(|data| {
281                // Feed bytes to H2.
282                if let Err(_e) = h2.recv(data) {
283                    return ParseResult::Consumed(data.len());
284                }
285
286                // Dispatch all events.
287                while let Some(event) = h2.poll_event() {
288                    match event {
289                        H2Event::SettingsAcknowledged => {
290                            *settings_acked = true;
291                        }
292                        H2Event::Response {
293                            stream_id,
294                            headers,
295                            end_stream,
296                        } => {
297                            if let Some(ps) = pending.get_mut(&stream_id) {
298                                // Extract :status pseudo-header.
299                                for h in &headers {
300                                    if h.name == b":status" {
301                                        if let Ok(s) = std::str::from_utf8(&h.value) {
302                                            ps.status = s.parse().ok();
303                                        }
304                                    } else {
305                                        let name = String::from_utf8_lossy(&h.name).into_owned();
306                                        let value = String::from_utf8_lossy(&h.value).into_owned();
307                                        if name.eq_ignore_ascii_case("content-encoding") {
308                                            ps.content_encoding = Some(value.clone());
309                                        }
310                                        ps.headers.push((name, value));
311                                    }
312                                }
313                                if end_stream {
314                                    ps.done = true;
315                                }
316                            }
317                        }
318                        H2Event::Data {
319                            stream_id,
320                            data: payload,
321                            end_stream,
322                        } => {
323                            if let Some(ps) = pending.get_mut(&stream_id) {
324                                if ps.streaming {
325                                    ps.chunks.push_back(Bytes::from(payload));
326                                } else {
327                                    ps.body.extend_from_slice(&payload);
328                                }
329                                if end_stream {
330                                    ps.done = true;
331                                }
332                            }
333                        }
334                        H2Event::Trailers { stream_id, .. } => {
335                            if let Some(ps) = pending.get_mut(&stream_id) {
336                                ps.done = true;
337                            }
338                        }
339                        H2Event::StreamReset { stream_id, .. } => {
340                            if let Some(ps) = pending.get_mut(&stream_id) {
341                                ps.done = true;
342                            }
343                        }
344                        H2Event::GoAway { .. } => {
345                            // Mark all pending streams as done.
346                            for ps in pending.values_mut() {
347                                ps.done = true;
348                            }
349                        }
350                        H2Event::Error(_) => {}
351                        H2Event::PingAcknowledged { .. } => {}
352                    }
353                }
354
355                // Retry blocked sends — flow control windows may have opened.
356                let mut retry = VecDeque::new();
357                std::mem::swap(blocked, &mut retry);
358                for bs in retry {
359                    if let Err(_e) = h2.send_data(bs.stream_id, &bs.data, bs.end_stream) {
360                        // Still blocked, re-queue.
361                        blocked.push_back(bs);
362                    }
363                }
364
365                // H2 buffers internally, consume all input.
366                ParseResult::Consumed(data.len())
367            })
368            .await;
369
370        if n == 0 {
371            return Err(HttpError::ConnectionClosed);
372        }
373
374        // Move completed non-streaming streams to the completed queue.
375        let done_ids: Vec<u32> = self
376            .pending_streams
377            .iter()
378            .filter(|(_, ps)| ps.done && !ps.streaming)
379            .map(|(id, _)| *id)
380            .collect();
381        for id in done_ids {
382            if let Some(ps) = self.pending_streams.remove(&id) {
383                self.completed.push_back((id, ps.into_response()?));
384            }
385        }
386
387        // Flush protocol responses (SETTINGS ACK, WINDOW_UPDATE, PING ACK).
388        self.flush_pending_send()?;
389
390        Ok(())
391    }
392
393    // ── Streaming API ──────────────────────────────────────────────────
394
395    /// Send a request and return a streaming response after headers arrive.
396    ///
397    /// The caller must drain the body via [`H2StreamingResponse::next_chunk()`]
398    /// before issuing further requests on this connection.
399    pub async fn send_request_streaming(
400        &mut self,
401        method: &str,
402        path: &str,
403        host: &str,
404        extra_headers: &[(&str, &str)],
405        body: Option<&[u8]>,
406    ) -> Result<H2StreamingResponse<'_>, HttpError> {
407        let stream_id = self.fire_request(method, path, host, extra_headers, body)?;
408
409        // Mark the stream as streaming.
410        if let Some(ps) = self.pending_streams.get_mut(&stream_id) {
411            ps.streaming = true;
412        }
413
414        // Pump until headers arrive for this stream.
415        loop {
416            if let Some(ps) = self.pending_streams.get(&stream_id) {
417                if ps.status.is_some() {
418                    break;
419                }
420            } else {
421                return Err(HttpError::Protocol("stream vanished".into()));
422            }
423
424            self.pump_once().await?;
425        }
426
427        Ok(H2StreamingResponse {
428            conn: self,
429            stream_id,
430        })
431    }
432
433    /// Drain `h2.take_pending_send()` to `conn.send_nowait()`.
434    fn flush_pending_send(&mut self) -> Result<(), HttpError> {
435        let pending = self.h2.take_pending_send();
436        if !pending.is_empty() {
437            self.conn.send_nowait(&pending)?;
438        }
439        Ok(())
440    }
441}
442
443/// Streaming HTTP/2 response. Borrows the connection exclusively.
444///
445/// Body chunks are yielded one at a time via [`next_chunk()`](Self::next_chunk).
446/// When all chunks have been consumed (returns `Ok(None)`), the stream is
447/// cleaned up automatically. The stream is also cleaned up on drop.
448pub struct H2StreamingResponse<'a> {
449    conn: &'a mut H2AsyncConn,
450    stream_id: u32,
451}
452
453impl<'a> H2StreamingResponse<'a> {
454    /// HTTP status code.
455    pub fn status(&self) -> u16 {
456        self.conn
457            .pending_streams
458            .get(&self.stream_id)
459            .and_then(|ps| ps.status)
460            .unwrap_or(0)
461    }
462
463    /// Response headers as (name, value) pairs.
464    pub fn headers(&self) -> &[(String, String)] {
465        self.conn
466            .pending_streams
467            .get(&self.stream_id)
468            .map(|ps| ps.headers.as_slice())
469            .unwrap_or(&[])
470    }
471
472    /// Get the first header value matching `name` (case-insensitive).
473    pub fn header(&self, name: &str) -> Option<&str> {
474        let lower = name.to_ascii_lowercase();
475        self.headers()
476            .iter()
477            .find(|(k, _)| k.to_ascii_lowercase() == lower)
478            .map(|(_, v)| v.as_str())
479    }
480
481    /// Yield the next body chunk, or `None` when the body is complete.
482    pub async fn next_chunk(&mut self) -> Result<Option<Bytes>, HttpError> {
483        loop {
484            if let Some(ps) = self.conn.pending_streams.get_mut(&self.stream_id) {
485                // Return a buffered chunk if available.
486                if let Some(chunk) = ps.chunks.pop_front() {
487                    return Ok(Some(chunk));
488                }
489                // No more chunks and stream is done.
490                if ps.done {
491                    self.conn.pending_streams.remove(&self.stream_id);
492                    return Ok(None);
493                }
494            } else {
495                return Ok(None);
496            }
497
498            // Pump to get more data.
499            self.conn.pump_once().await?;
500        }
501    }
502}
503
504impl Drop for H2StreamingResponse<'_> {
505    fn drop(&mut self) {
506        self.conn.pending_streams.remove(&self.stream_id);
507    }
508}