Skip to main content

ringline_http/
h2_conn.rs

1//! Async HTTP/2 connection wrapping `H2Connection` with a pump loop.
2//!
3//! Uses a fire/recv pipelining pattern: fire requests synchronously, then pump
4//! the 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    /// Terminal error for this stream — set when validation fails or the
26    /// peer violates protocol. Surfaced as the stream's completion result.
27    error: Option<HttpError>,
28    /// When true, DATA payloads are pushed to `chunks` instead of `body`.
29    streaming: bool,
30    /// Buffered chunks for streaming responses.
31    chunks: VecDeque<Bytes>,
32    /// Content-Encoding from response headers (for decompression).
33    content_encoding: Option<String>,
34    /// Running total of header bytes (name + value) accepted so far,
35    /// bounded by `H2AsyncConn::max_header_section`.
36    header_bytes: usize,
37    /// Running total of body bytes accepted so far, bounded by
38    /// `H2AsyncConn::max_body_size`.
39    body_bytes: usize,
40}
41
42impl PendingStream {
43    fn new() -> Self {
44        Self {
45            status: None,
46            headers: Vec::new(),
47            body: BytesMut::new(),
48            done: false,
49            error: None,
50            streaming: false,
51            chunks: VecDeque::new(),
52            content_encoding: None,
53            header_bytes: 0,
54            body_bytes: 0,
55        }
56    }
57
58    /// Mark the stream terminally failed with `err`. First error wins —
59    /// later errors are ignored so the caller sees the underlying cause.
60    fn fail(&mut self, err: HttpError) {
61        if self.error.is_none() {
62            self.error = Some(err);
63        }
64        self.done = true;
65    }
66
67    #[cfg_attr(
68        not(any(feature = "gzip", feature = "zstd", feature = "brotli")),
69        allow(unused_variables)
70    )]
71    fn into_response(self, max_decompressed_size: usize) -> Result<Response, HttpError> {
72        if let Some(err) = self.error {
73            return Err(err);
74        }
75        // :status must have been set and validated before the stream
76        // completes — into_response should only run for non-error
77        // completions, and the parser sets `error` when :status is bad.
78        let status = self
79            .status
80            .ok_or_else(|| HttpError::InvalidMessage("response missing :status".into()))?;
81
82        // Decompress body if Content-Encoding is set.
83        if let Some(ref encoding) = self.content_encoding
84            && !is_identity_encoding(encoding)
85        {
86            #[cfg(any(feature = "gzip", feature = "zstd", feature = "brotli"))]
87            {
88                let decompressed =
89                    crate::compress::decompress(encoding, &self.body, max_decompressed_size)?;
90                return Ok(Response::new(
91                    status,
92                    self.headers,
93                    Bytes::from(decompressed),
94                ));
95            }
96            // Compression features off — we cannot validate the body
97            // matches the declared encoding. Refusing to silently hand a
98            // gzipped payload to the caller as plaintext (silent data
99            // corruption class). Mirrors ringline-grpc PR #174 finding 4.
100            #[cfg(not(any(feature = "gzip", feature = "zstd", feature = "brotli")))]
101            {
102                return Err(HttpError::InvalidMessage(format!(
103                    "unsupported content-encoding `{encoding}` (no decompressor compiled in)"
104                )));
105            }
106        }
107
108        Ok(Response::new(status, self.headers, self.body.freeze()))
109    }
110}
111
112/// `true` when the Content-Encoding is empty/`identity`, meaning the
113/// body is delivered as-is and no decompression is required.
114fn is_identity_encoding(encoding: &str) -> bool {
115    let e = encoding.trim();
116    e.is_empty() || e.eq_ignore_ascii_case("identity")
117}
118
119/// Parse and validate an HTTP/2 `:status` pseudo-header value per RFC
120/// 9110 §15: exactly three ASCII digits, value 100–999. Anything else
121/// (missing, non-numeric, wrong length, out of range) is a protocol
122/// violation — returning `None` lets the caller fail the stream with a
123/// specific error instead of silently substituting 0 (a real HTTP status
124/// code does not exist outside this range).
125fn parse_status(value: &[u8]) -> Option<u16> {
126    if value.len() != 3 {
127        return None;
128    }
129    if !value.iter().all(|b| b.is_ascii_digit()) {
130        return None;
131    }
132    let s = std::str::from_utf8(value).ok()?;
133    let n: u16 = s.parse().ok()?;
134    if (100..=999).contains(&n) {
135        Some(n)
136    } else {
137        None
138    }
139}
140
141/// Apply a HEADERS event to a pending stream, validating `:status` and
142/// enforcing the per-stream header-section cap.
143fn handle_response_headers(
144    ps: &mut PendingStream,
145    headers: &[HeaderField],
146    end_stream: bool,
147    max_header_section: usize,
148) {
149    for h in headers {
150        if h.name == b":status" {
151            match parse_status(&h.value) {
152                Some(n) => ps.status = Some(n),
153                None => {
154                    ps.fail(HttpError::InvalidMessage(format!(
155                        "invalid :status `{}`",
156                        String::from_utf8_lossy(&h.value)
157                    )));
158                    return;
159                }
160            }
161            continue;
162        }
163
164        // Track the running header-section size. Counting name + value
165        // bytes (no per-entry overhead) mirrors HPACK accounting and
166        // keeps the cap interpretable for callers configuring it.
167        let add = h.name.len().saturating_add(h.value.len());
168        ps.header_bytes = ps.header_bytes.saturating_add(add);
169        if ps.header_bytes > max_header_section {
170            ps.fail(HttpError::MaxSizeExceeded(format!(
171                "response header section exceeds {max_header_section} bytes"
172            )));
173            return;
174        }
175
176        let name = String::from_utf8_lossy(&h.name).into_owned();
177        let value = String::from_utf8_lossy(&h.value).into_owned();
178        if name.eq_ignore_ascii_case("content-encoding") {
179            ps.content_encoding = Some(value.clone());
180        }
181        ps.headers.push((name, value));
182    }
183    if end_stream {
184        // If end_stream arrives without `:status`, surface it now so the
185        // caller doesn't get a successful response with status 0.
186        if ps.status.is_none() && ps.error.is_none() {
187            ps.fail(HttpError::InvalidMessage(
188                "response HEADERS missing :status".into(),
189            ));
190        } else {
191            ps.done = true;
192        }
193    }
194}
195
196/// Apply a DATA event to a pending stream, enforcing the per-stream
197/// body-size cap.
198fn handle_response_data(
199    ps: &mut PendingStream,
200    payload: Vec<u8>,
201    end_stream: bool,
202    max_body_size: usize,
203) {
204    let new_total = ps.body_bytes.saturating_add(payload.len());
205    if new_total > max_body_size {
206        ps.fail(HttpError::MaxSizeExceeded(format!(
207            "response body exceeds {max_body_size} bytes"
208        )));
209        return;
210    }
211    ps.body_bytes = new_total;
212    if ps.streaming {
213        ps.chunks.push_back(Bytes::from(payload));
214    } else {
215        ps.body.extend_from_slice(&payload);
216    }
217    if end_stream {
218        ps.done = true;
219    }
220}
221
222/// Data for a send blocked on flow control.
223struct BlockedSend {
224    stream_id: u32,
225    data: Vec<u8>,
226    end_stream: bool,
227}
228
229/// Async HTTP/2 connection with multiplexed request support.
230///
231/// Wraps a sans-IO `H2Connection` and a `ConnCtx`, providing a pump loop
232/// that bridges bytes between the transport and the H2 state machine.
233pub struct H2AsyncConn {
234    conn: ConnCtx,
235    h2: H2Connection,
236    pending_streams: HashMap<u32, PendingStream>,
237    blocked_sends: VecDeque<BlockedSend>,
238    /// Streams that completed during a pump cycle, ready for pickup.
239    /// Each entry carries either a successful response or a per-stream
240    /// error (bad `:status`, oversize headers/body, unsupported encoding).
241    completed: VecDeque<(u32, Result<Response, HttpError>)>,
242    settings_acked: bool,
243    /// Cap on a decompressed response body. Defaults to 64 MiB.
244    max_decompressed_size: usize,
245    /// Cap on the total bytes (name + value) accumulated per stream's
246    /// response header section. Defaults to 64 KiB.
247    max_header_section: usize,
248    /// Cap on a single stream's accumulated response body. Defaults to
249    /// 16 MiB.
250    max_body_size: usize,
251    /// Set once the peer has sent GOAWAY — this connection must not be
252    /// used for further requests.
253    goaway_received: bool,
254}
255
256impl H2AsyncConn {
257    /// Override the cap on a decompressed response body. Default 64 MiB —
258    /// defends against decompression bombs.
259    pub fn set_max_decompressed_size(&mut self, n: usize) {
260        self.max_decompressed_size = n;
261    }
262
263    /// Override the cap on the total response header bytes (sum of name +
264    /// value lengths) collected per stream. Default 64 KiB.
265    pub fn set_max_header_section(&mut self, n: usize) {
266        self.max_header_section = n;
267    }
268
269    /// Override the cap on a single stream's accumulated response body.
270    /// Default 16 MiB.
271    pub fn set_max_body_size(&mut self, n: usize) {
272        self.max_body_size = n;
273    }
274
275    /// Whether the peer has signalled it will not accept further requests
276    /// (GOAWAY received). When `true`, callers should reconnect rather
277    /// than reuse this connection.
278    pub fn peer_will_close(&self) -> bool {
279        self.goaway_received
280    }
281}
282
283impl H2AsyncConn {
284    /// Connect to an HTTP/2 server over TLS.
285    ///
286    /// Performs TLS handshake, sends the H2 connection preface, and waits
287    /// for the server SETTINGS exchange to complete.
288    pub async fn connect(addr: SocketAddr, host: &str) -> Result<Self, HttpError> {
289        let conn = ringline::connect_tls(addr, host)?.await?;
290        Self::from_conn(conn).await
291    }
292
293    /// Connect with a timeout (milliseconds).
294    pub async fn connect_with_timeout(
295        addr: SocketAddr,
296        host: &str,
297        timeout_ms: u64,
298    ) -> Result<Self, HttpError> {
299        let conn = ringline::connect_tls_with_timeout(addr, host, timeout_ms)?.await?;
300        Self::from_conn(conn).await
301    }
302
303    /// Wrap an already-connected `ConnCtx` (must be TLS for H2).
304    ///
305    /// Sends the H2 preface and waits for SETTINGS exchange.
306    pub async fn from_conn(conn: ConnCtx) -> Result<Self, HttpError> {
307        let h2 = H2Connection::new(Settings::client_default());
308
309        let mut this = Self {
310            conn,
311            h2,
312            pending_streams: HashMap::new(),
313            blocked_sends: VecDeque::new(),
314            completed: VecDeque::new(),
315            settings_acked: false,
316            max_decompressed_size: crate::compress::DEFAULT_MAX_DECOMPRESSED_SIZE,
317            max_header_section: crate::h1_conn::DEFAULT_MAX_HEADER_SECTION,
318            max_body_size: crate::h1_conn::DEFAULT_MAX_BODY_SIZE,
319            goaway_received: false,
320        };
321
322        // Send the connection preface (magic + SETTINGS).
323        this.flush_pending_send()?;
324
325        // Pump until we get SettingsAcknowledged.
326        while !this.settings_acked {
327            this.pump_once().await?;
328        }
329
330        Ok(this)
331    }
332
333    /// Returns the underlying connection context.
334    pub fn close(&self) {
335        self.conn.close();
336    }
337
338    pub fn conn(&self) -> ConnCtx {
339        self.conn
340    }
341
342    /// Number of in-flight streams.
343    pub fn pending_count(&self) -> usize {
344        self.pending_streams.len()
345    }
346
347    // ── Sequential API ─────────────────────────────────────────────────
348
349    /// Send a request and wait for the complete response.
350    pub async fn send_request(
351        &mut self,
352        method: &str,
353        path: &str,
354        host: &str,
355        extra_headers: &[(&str, &str)],
356        body: Option<&[u8]>,
357    ) -> Result<Response, HttpError> {
358        let stream_id = self.fire_request(method, path, host, extra_headers, body)?;
359        self.recv_stream(stream_id).await
360    }
361
362    // ── Multiplexed fire API ───────────────────────────────────────────
363
364    /// Fire an HTTP/2 request. Returns the stream ID immediately.
365    ///
366    /// The request is queued for sending; call `recv()` or `recv_stream()`
367    /// to pump the connection and collect responses.
368    pub fn fire_request(
369        &mut self,
370        method: &str,
371        path: &str,
372        host: &str,
373        extra_headers: &[(&str, &str)],
374        body: Option<&[u8]>,
375    ) -> Result<u32, HttpError> {
376        let has_body = body.is_some_and(|b| !b.is_empty());
377
378        let mut headers = vec![
379            HeaderField::new(b":method", method.as_bytes()),
380            HeaderField::new(b":path", path.as_bytes()),
381            HeaderField::new(b":scheme", b"https"),
382            HeaderField::new(b":authority", host.as_bytes()),
383        ];
384
385        let has_accept_encoding = extra_headers
386            .iter()
387            .any(|(name, _)| name.eq_ignore_ascii_case("accept-encoding"));
388
389        for (name, value) in extra_headers {
390            headers.push(HeaderField::new(name.as_bytes(), value.as_bytes()));
391        }
392
393        // Auto-inject Accept-Encoding when compression features are enabled
394        // and the caller has not already set one.
395        if !has_accept_encoding && let Some(ae) = crate::compress::accept_encoding_value() {
396            headers.push(HeaderField::new(b"accept-encoding", ae.as_bytes()));
397        }
398
399        let end_stream = !has_body;
400        let stream_id = self.h2.send_request(&headers, end_stream)?;
401
402        if let Some(data) = body
403            && !data.is_empty()
404            && let Err(e) = self.h2.send_data(stream_id, data, true)
405        {
406            if matches!(e, ringline_h2::H2Error::FlowControlError) {
407                self.blocked_sends.push_back(BlockedSend {
408                    stream_id,
409                    data: data.to_vec(),
410                    end_stream: true,
411                });
412            } else {
413                return Err(HttpError::H2(e));
414            }
415        }
416
417        self.pending_streams.insert(stream_id, PendingStream::new());
418
419        // Flush the request frames to the transport.
420        self.flush_pending_send()?;
421
422        Ok(stream_id)
423    }
424
425    // ── Multiplexed recv API ───────────────────────────────────────────
426
427    /// Pump until any stream completes. Returns `(stream_id, Response)`,
428    /// or surfaces a per-stream `HttpError` for that stream.
429    pub async fn recv(&mut self) -> Result<(u32, Response), HttpError> {
430        // Check if we already have a completed response queued.
431        if let Some((id, result)) = self.completed.pop_front() {
432            return result.map(|r| (id, r));
433        }
434
435        loop {
436            self.pump_once().await?;
437
438            if let Some((id, result)) = self.completed.pop_front() {
439                return result.map(|r| (id, r));
440            }
441        }
442    }
443
444    /// Pump until a specific stream completes.
445    pub async fn recv_stream(&mut self, stream_id: u32) -> Result<Response, HttpError> {
446        // Check completed queue first.
447        if let Some(idx) = self.completed.iter().position(|(sid, _)| *sid == stream_id) {
448            let (_, result) = self.completed.remove(idx).unwrap();
449            return result;
450        }
451
452        loop {
453            self.pump_once().await?;
454
455            // Check if our target stream completed.
456            if let Some(idx) = self.completed.iter().position(|(sid, _)| *sid == stream_id) {
457                let (_, result) = self.completed.remove(idx).unwrap();
458                return result;
459            }
460        }
461    }
462
463    // ── Internal pump loop ─────────────────────────────────────────────
464
465    /// One round of the pump loop:
466    /// 1. Flush pending H2 output to transport
467    /// 2. Read bytes from transport, feed to H2
468    /// 3. Dispatch H2 events to pending streams
469    /// 4. Retry blocked sends (flow control may have opened)
470    /// 5. Flush any protocol responses (WINDOW_UPDATE, PING ACK, etc.)
471    async fn pump_once(&mut self) -> Result<(), HttpError> {
472        // Flush any pending output before blocking on recv.
473        self.flush_pending_send()?;
474
475        // Borrow-split: capture mutable refs before the closure.
476        let h2 = &mut self.h2;
477        let pending = &mut self.pending_streams;
478        let blocked = &mut self.blocked_sends;
479        let settings_acked = &mut self.settings_acked;
480        let goaway_received = &mut self.goaway_received;
481        let max_header_section = self.max_header_section;
482        let max_body_size = self.max_body_size;
483        // Connection-level error captured during dispatch (recv error,
484        // unrecoverable send error, H2Event::Error). The pump returns
485        // this once the closure unwinds.
486        let mut connection_error: Option<HttpError> = None;
487
488        let n = self
489            .conn
490            .with_data(|data| {
491                // Feed bytes to H2.
492                if let Err(e) = h2.recv(data) {
493                    connection_error = Some(HttpError::H2(e));
494                    return ParseResult::Consumed(data.len());
495                }
496
497                // Dispatch all events.
498                while let Some(event) = h2.poll_event() {
499                    match event {
500                        H2Event::SettingsAcknowledged => {
501                            *settings_acked = true;
502                        }
503                        H2Event::Response {
504                            stream_id,
505                            headers,
506                            end_stream,
507                        } => {
508                            if let Some(ps) = pending.get_mut(&stream_id) {
509                                handle_response_headers(
510                                    ps,
511                                    &headers,
512                                    end_stream,
513                                    max_header_section,
514                                );
515                            }
516                        }
517                        H2Event::Data {
518                            stream_id,
519                            data: payload,
520                            end_stream,
521                        } => {
522                            if let Some(ps) = pending.get_mut(&stream_id) {
523                                handle_response_data(ps, payload, end_stream, max_body_size);
524                            }
525                        }
526                        H2Event::Trailers { stream_id, .. } => {
527                            if let Some(ps) = pending.get_mut(&stream_id) {
528                                ps.done = true;
529                            }
530                        }
531                        H2Event::StreamReset {
532                            stream_id,
533                            error_code,
534                        } => {
535                            if let Some(ps) = pending.get_mut(&stream_id) {
536                                ps.fail(HttpError::H2(ringline_h2::H2Error::StreamError(
537                                    stream_id, error_code,
538                                )));
539                            }
540                        }
541                        H2Event::GoAway { .. } => {
542                            *goaway_received = true;
543                            // Mark all pending streams as done. Streams
544                            // that already received complete responses
545                            // resolve normally; streams still mid-response
546                            // surface a ConnectionClosed-shaped error
547                            // when into_response runs (missing :status →
548                            // InvalidMessage).
549                            for ps in pending.values_mut() {
550                                ps.done = true;
551                            }
552                        }
553                        H2Event::Error(e) => {
554                            // Connection-level protocol error from the
555                            // sans-IO state machine. The connection is
556                            // dead; surface to the caller instead of
557                            // silently hanging.
558                            connection_error = Some(HttpError::H2(e));
559                        }
560                        H2Event::PingAcknowledged { .. } => {}
561                    }
562                }
563
564                // Retry blocked sends — flow control windows may have opened.
565                let mut retry = VecDeque::new();
566                std::mem::swap(blocked, &mut retry);
567                for bs in retry {
568                    match h2.send_data(bs.stream_id, &bs.data, bs.end_stream) {
569                        Ok(()) => {}
570                        Err(ringline_h2::H2Error::FlowControlError) => {
571                            // Still blocked, re-queue.
572                            blocked.push_back(bs);
573                        }
574                        Err(e) => {
575                            // Stream gone or other unrecoverable send
576                            // failure — fail the originating stream so
577                            // the caller learns the request didn't ship.
578                            if let Some(ps) = pending.get_mut(&bs.stream_id) {
579                                ps.fail(HttpError::H2(e));
580                            }
581                        }
582                    }
583                }
584
585                // H2 buffers internally, consume all input.
586                ParseResult::Consumed(data.len())
587            })
588            .await;
589
590        if let Some(e) = connection_error {
591            return Err(e);
592        }
593        if n == 0 {
594            return Err(HttpError::ConnectionClosed);
595        }
596
597        // Move completed non-streaming streams to the completed queue.
598        let done_ids: Vec<u32> = self
599            .pending_streams
600            .iter()
601            .filter(|(_, ps)| ps.done && !ps.streaming)
602            .map(|(id, _)| *id)
603            .collect();
604        for id in done_ids {
605            if let Some(ps) = self.pending_streams.remove(&id) {
606                let result = ps.into_response(self.max_decompressed_size);
607                self.completed.push_back((id, result));
608            }
609        }
610
611        // Flush protocol responses (SETTINGS ACK, WINDOW_UPDATE, PING ACK).
612        self.flush_pending_send()?;
613
614        Ok(())
615    }
616
617    // ── Streaming API ──────────────────────────────────────────────────
618
619    /// Send a request and return a streaming response after headers arrive.
620    ///
621    /// The caller must drain the body via [`H2StreamingResponse::next_chunk()`]
622    /// before issuing further requests on this connection.
623    pub async fn send_request_streaming(
624        &mut self,
625        method: &str,
626        path: &str,
627        host: &str,
628        extra_headers: &[(&str, &str)],
629        body: Option<&[u8]>,
630    ) -> Result<H2StreamingResponse<'_>, HttpError> {
631        let stream_id = self.fire_request(method, path, host, extra_headers, body)?;
632
633        // Mark the stream as streaming.
634        if let Some(ps) = self.pending_streams.get_mut(&stream_id) {
635            ps.streaming = true;
636        }
637
638        // Pump until headers arrive for this stream — or it errors.
639        loop {
640            if let Some(ps) = self.pending_streams.get_mut(&stream_id) {
641                if let Some(err) = ps.error.take() {
642                    self.pending_streams.remove(&stream_id);
643                    return Err(err);
644                }
645                if ps.status.is_some() {
646                    break;
647                }
648            } else {
649                return Err(HttpError::Protocol("stream vanished".into()));
650            }
651
652            self.pump_once().await?;
653        }
654
655        Ok(H2StreamingResponse {
656            conn: self,
657            stream_id,
658        })
659    }
660
661    /// Drain `h2.take_pending_send()` to `conn.send_nowait()`.
662    fn flush_pending_send(&mut self) -> Result<(), HttpError> {
663        let pending = self.h2.take_pending_send();
664        if !pending.is_empty() {
665            self.conn.send_nowait(&pending)?;
666        }
667        Ok(())
668    }
669}
670
671/// Streaming HTTP/2 response. Borrows the connection exclusively.
672///
673/// Body chunks are yielded one at a time via [`next_chunk()`](Self::next_chunk).
674/// When all chunks have been consumed (returns `Ok(None)`), the stream is
675/// cleaned up automatically. The stream is also cleaned up on drop.
676pub struct H2StreamingResponse<'a> {
677    conn: &'a mut H2AsyncConn,
678    stream_id: u32,
679}
680
681impl<'a> H2StreamingResponse<'a> {
682    /// HTTP status code.
683    pub fn status(&self) -> u16 {
684        self.conn
685            .pending_streams
686            .get(&self.stream_id)
687            .and_then(|ps| ps.status)
688            .unwrap_or(0)
689    }
690
691    /// Response headers as (name, value) pairs.
692    pub fn headers(&self) -> &[(String, String)] {
693        self.conn
694            .pending_streams
695            .get(&self.stream_id)
696            .map(|ps| ps.headers.as_slice())
697            .unwrap_or(&[])
698    }
699
700    /// Get the first header value matching `name` (case-insensitive).
701    pub fn header(&self, name: &str) -> Option<&str> {
702        let lower = name.to_ascii_lowercase();
703        self.headers()
704            .iter()
705            .find(|(k, _)| k.to_ascii_lowercase() == lower)
706            .map(|(_, v)| v.as_str())
707    }
708
709    /// Yield the next body chunk, or `None` when the body is complete.
710    pub async fn next_chunk(&mut self) -> Result<Option<Bytes>, HttpError> {
711        loop {
712            if let Some(ps) = self.conn.pending_streams.get_mut(&self.stream_id) {
713                // Surface a deferred per-stream error (body cap exceeded,
714                // unrecoverable send-data failure) before yielding more
715                // bytes. Take it so we only report it once.
716                if let Some(err) = ps.error.take() {
717                    self.conn.pending_streams.remove(&self.stream_id);
718                    return Err(err);
719                }
720                // Return a buffered chunk if available.
721                if let Some(chunk) = ps.chunks.pop_front() {
722                    return Ok(Some(chunk));
723                }
724                // No more chunks and stream is done.
725                if ps.done {
726                    self.conn.pending_streams.remove(&self.stream_id);
727                    return Ok(None);
728                }
729            } else {
730                return Ok(None);
731            }
732
733            // Pump to get more data.
734            self.conn.pump_once().await?;
735        }
736    }
737}
738
739impl Drop for H2StreamingResponse<'_> {
740    fn drop(&mut self) {
741        self.conn.pending_streams.remove(&self.stream_id);
742    }
743}
744
745#[cfg(test)]
746mod tests {
747    //! Unit tests for the I/O-free helpers and `PendingStream` state
748    //! machine. The full pump loop is exercised by the public-servers
749    //! integration tests (`tests/public_servers.rs`).
750
751    use super::*;
752
753    fn header(name: &[u8], value: &[u8]) -> HeaderField {
754        HeaderField::new(name, value)
755    }
756
757    // ── parse_status ───────────────────────────────────────────────────
758
759    #[test]
760    fn parse_status_accepts_three_digit_codes() {
761        assert_eq!(parse_status(b"200"), Some(200));
762        assert_eq!(parse_status(b"404"), Some(404));
763        assert_eq!(parse_status(b"100"), Some(100));
764        assert_eq!(parse_status(b"599"), Some(599));
765        assert_eq!(parse_status(b"999"), Some(999));
766    }
767
768    #[test]
769    fn parse_status_rejects_wrong_length() {
770        assert_eq!(parse_status(b""), None);
771        assert_eq!(parse_status(b"2"), None);
772        assert_eq!(parse_status(b"20"), None);
773        assert_eq!(parse_status(b"2000"), None);
774    }
775
776    #[test]
777    fn parse_status_rejects_non_digits() {
778        assert_eq!(parse_status(b"abc"), None);
779        assert_eq!(parse_status(b"2x0"), None);
780        assert_eq!(parse_status(b"-10"), None);
781    }
782
783    #[test]
784    fn parse_status_rejects_out_of_range() {
785        // 099 fails the >= 100 check.
786        assert_eq!(parse_status(b"099"), None);
787        // No real HTTP status sits below 100 per RFC 9110 §15.
788    }
789
790    // ── handle_response_headers (4a, 4f) ───────────────────────────────
791
792    #[test]
793    fn invalid_status_fails_stream() {
794        let mut ps = PendingStream::new();
795        let headers = vec![header(b":status", b"oops")];
796        handle_response_headers(&mut ps, &headers, true, 64 * 1024);
797        assert!(ps.error.is_some());
798        match ps.error.unwrap() {
799            HttpError::InvalidMessage(_) => {}
800            other => panic!("expected InvalidMessage, got {other:?}"),
801        }
802    }
803
804    #[test]
805    fn missing_status_with_end_stream_fails() {
806        let mut ps = PendingStream::new();
807        let headers = vec![header(b"content-type", b"text/plain")];
808        handle_response_headers(&mut ps, &headers, true, 64 * 1024);
809        assert!(matches!(ps.error, Some(HttpError::InvalidMessage(_))));
810    }
811
812    #[test]
813    fn oversize_header_section_fails_stream() {
814        let mut ps = PendingStream::new();
815        // 200 bytes of value plus a small name pushes past the 100-byte cap.
816        let big_value = vec![b'x'; 200];
817        let headers = vec![header(b":status", b"200"), header(b"x", &big_value)];
818        handle_response_headers(&mut ps, &headers, false, 100);
819        assert!(matches!(ps.error, Some(HttpError::MaxSizeExceeded(_))));
820    }
821
822    #[test]
823    fn valid_status_accepts_and_collects_headers() {
824        let mut ps = PendingStream::new();
825        let headers = vec![header(b":status", b"204"), header(b"x-custom", b"value")];
826        handle_response_headers(&mut ps, &headers, true, 64 * 1024);
827        assert_eq!(ps.status, Some(204));
828        assert!(ps.error.is_none());
829        assert!(ps.done);
830        assert_eq!(
831            ps.headers,
832            vec![("x-custom".to_string(), "value".to_string())]
833        );
834    }
835
836    // ── handle_response_data (4f) ──────────────────────────────────────
837
838    #[test]
839    fn oversize_body_fails_stream() {
840        let mut ps = PendingStream::new();
841        let payload = vec![0u8; 200];
842        handle_response_data(&mut ps, payload, false, 100);
843        assert!(matches!(ps.error, Some(HttpError::MaxSizeExceeded(_))));
844    }
845
846    #[test]
847    fn body_within_cap_accumulates() {
848        let mut ps = PendingStream::new();
849        handle_response_data(&mut ps, vec![1, 2, 3], false, 100);
850        handle_response_data(&mut ps, vec![4, 5], true, 100);
851        assert!(ps.error.is_none());
852        assert!(ps.done);
853        assert_eq!(&ps.body[..], &[1, 2, 3, 4, 5]);
854    }
855
856    // ── into_response (4a, 4e) ─────────────────────────────────────────
857
858    #[test]
859    fn into_response_surfaces_stream_error() {
860        let mut ps = PendingStream::new();
861        ps.fail(HttpError::InvalidMessage("test".into()));
862        let res = ps.into_response(usize::MAX);
863        assert!(matches!(res, Err(HttpError::InvalidMessage(_))));
864    }
865
866    #[cfg(not(any(feature = "gzip", feature = "zstd", feature = "brotli")))]
867    #[test]
868    fn into_response_rejects_unsupported_content_encoding_without_features() {
869        let mut ps = PendingStream::new();
870        ps.status = Some(200);
871        ps.content_encoding = Some("gzip".into());
872        ps.body.extend_from_slice(&[1, 2, 3]);
873        let res = ps.into_response(usize::MAX);
874        match res {
875            Err(HttpError::InvalidMessage(msg)) => assert!(msg.contains("gzip")),
876            other => panic!("expected InvalidMessage for gzip without features, got {other:?}"),
877        }
878    }
879
880    #[test]
881    fn into_response_identity_encoding_returns_body_as_is() {
882        let mut ps = PendingStream::new();
883        ps.status = Some(200);
884        ps.content_encoding = Some("identity".into());
885        ps.body.extend_from_slice(&[1, 2, 3]);
886        let res = ps.into_response(usize::MAX);
887        let resp = res.expect("identity encoding must not error");
888        assert_eq!(resp.status(), 200);
889        assert_eq!(resp.bytes().as_ref(), &[1, 2, 3]);
890    }
891
892    // ── is_identity_encoding ───────────────────────────────────────────
893
894    #[test]
895    fn is_identity_encoding_recognises_variants() {
896        assert!(is_identity_encoding(""));
897        assert!(is_identity_encoding("identity"));
898        assert!(is_identity_encoding("Identity"));
899        assert!(is_identity_encoding("  identity  "));
900        assert!(!is_identity_encoding("gzip"));
901        assert!(!is_identity_encoding("br"));
902    }
903}