Skip to main content

ringline_http/
h1_conn.rs

1//! HTTP/1.1 connection.
2//!
3//! Simple request-response on a single `ConnCtx`. No multiplexing — each
4//! request blocks until its response is fully received.
5
6use std::net::SocketAddr;
7
8use bytes::{Bytes, BytesMut};
9use ringline::{ConnCtx, ParseResult};
10
11use crate::error::HttpError;
12use crate::response::Response;
13
14/// An HTTP/1.1 connection wrapping a `ConnCtx`.
15pub struct H1Conn {
16    conn: ConnCtx,
17    host: String,
18}
19
20impl H1Conn {
21    /// Connect to an HTTP/1.1 server over TLS.
22    pub async fn connect_tls(addr: SocketAddr, host: &str) -> Result<Self, HttpError> {
23        let conn = ringline::connect_tls(addr, host)?.await?;
24        Ok(Self {
25            conn,
26            host: host.to_string(),
27        })
28    }
29
30    /// Connect to an HTTP/1.1 server over plaintext TCP.
31    pub async fn connect_plain(addr: SocketAddr, host: &str) -> Result<Self, HttpError> {
32        let conn = ringline::connect(addr)?.await?;
33        Ok(Self {
34            conn,
35            host: host.to_string(),
36        })
37    }
38
39    /// Returns the underlying connection context.
40    pub fn conn(&self) -> ConnCtx {
41        self.conn
42    }
43
44    /// Returns the host name.
45    pub fn close(&self) {
46        self.conn.close();
47    }
48
49    pub fn host(&self) -> &str {
50        &self.host
51    }
52
53    /// Send an HTTP/1.1 request and receive the response.
54    pub async fn send_request(
55        &mut self,
56        method: &str,
57        path: &str,
58        extra_headers: &[(&str, &str)],
59        body: Option<&[u8]>,
60    ) -> Result<Response, HttpError> {
61        let hdr = self
62            .send_and_parse_headers(method, path, extra_headers, body)
63            .await?;
64
65        let mut body_buf = hdr.body_leftover;
66
67        // Phase 2: Read body.
68        if let Some(cl) = hdr.content_length {
69            // Content-Length body.
70            while body_buf.len() < cl {
71                let target_len = cl;
72                let n = self
73                    .conn
74                    .with_data(|data| {
75                        body_buf.extend_from_slice(data);
76                        if body_buf.len() >= target_len {
77                            ParseResult::Consumed(data.len())
78                        } else {
79                            // Consume what we got, need more.
80                            ParseResult::Consumed(data.len())
81                        }
82                    })
83                    .await;
84
85                if n == 0 {
86                    break;
87                }
88            }
89            body_buf.truncate(cl);
90        } else if hdr.chunked {
91            // Chunked transfer encoding.
92            let mut decoded = BytesMut::new();
93            let mut leftover = body_buf.to_vec();
94            body_buf.clear();
95
96            loop {
97                match decode_chunk(&leftover) {
98                    ChunkResult::Complete {
99                        data,
100                        consumed,
101                        is_last,
102                    } => {
103                        decoded.extend_from_slice(data);
104                        leftover = leftover[consumed..].to_vec();
105                        if is_last {
106                            break;
107                        }
108                    }
109                    ChunkResult::NeedMore => {
110                        // Read more data.
111                        let n = self
112                            .conn
113                            .with_data(|data| {
114                                leftover.extend_from_slice(data);
115                                ParseResult::Consumed(data.len())
116                            })
117                            .await;
118
119                        if n == 0 {
120                            break;
121                        }
122                    }
123                }
124            }
125
126            body_buf = decoded;
127        }
128        // else: no body (e.g. HEAD response, 204, 304)
129
130        // Decompress body if Content-Encoding is set.
131        #[cfg(any(feature = "gzip", feature = "zstd", feature = "brotli"))]
132        if let Some(ref encoding) = hdr.content_encoding {
133            let decompressed = crate::compress::decompress(encoding, &body_buf)?;
134            return Ok(Response::new(
135                hdr.status,
136                hdr.headers,
137                bytes::Bytes::from(decompressed),
138            ));
139        }
140
141        Ok(Response::new(hdr.status, hdr.headers, body_buf.freeze()))
142    }
143
144    /// Send a request and return a streaming response after headers arrive.
145    ///
146    /// The caller must drain the body via [`H1StreamingResponse::next_chunk()`]
147    /// before issuing further requests on this connection.
148    pub async fn send_request_streaming(
149        &mut self,
150        method: &str,
151        path: &str,
152        extra_headers: &[(&str, &str)],
153        body: Option<&[u8]>,
154    ) -> Result<H1StreamingResponse<'_>, HttpError> {
155        let hdr = self
156            .send_and_parse_headers(method, path, extra_headers, body)
157            .await?;
158
159        let state = if let Some(cl) = hdr.content_length {
160            H1StreamState::ContentLength {
161                remaining: cl.saturating_sub(hdr.body_leftover.len()),
162                leftover: hdr.body_leftover,
163            }
164        } else if hdr.chunked {
165            H1StreamState::Chunked {
166                leftover: hdr.body_leftover.to_vec(),
167            }
168        } else {
169            H1StreamState::Done
170        };
171
172        Ok(H1StreamingResponse {
173            conn: &mut self.conn,
174            status: hdr.status,
175            headers: hdr.headers,
176            state,
177        })
178    }
179
180    /// Serialize the request, send it, and parse response headers.
181    async fn send_and_parse_headers(
182        &mut self,
183        method: &str,
184        path: &str,
185        extra_headers: &[(&str, &str)],
186        body: Option<&[u8]>,
187    ) -> Result<H1HeaderResult, HttpError> {
188        // Serialize the request.
189        let mut req = Vec::with_capacity(256);
190        req.extend_from_slice(method.as_bytes());
191        req.push(b' ');
192        req.extend_from_slice(path.as_bytes());
193        req.extend_from_slice(b" HTTP/1.1\r\n");
194        req.extend_from_slice(b"host: ");
195        req.extend_from_slice(self.host.as_bytes());
196        req.extend_from_slice(b"\r\n");
197
198        let has_accept_encoding = extra_headers
199            .iter()
200            .any(|(name, _)| name.eq_ignore_ascii_case("accept-encoding"));
201
202        for (name, value) in extra_headers {
203            req.extend_from_slice(name.as_bytes());
204            req.extend_from_slice(b": ");
205            req.extend_from_slice(value.as_bytes());
206            req.extend_from_slice(b"\r\n");
207        }
208
209        // Auto-inject Accept-Encoding when compression features are enabled
210        // and the caller has not already set one.
211        if !has_accept_encoding && let Some(ae) = crate::compress::accept_encoding_value() {
212            req.extend_from_slice(b"accept-encoding: ");
213            req.extend_from_slice(ae.as_bytes());
214            req.extend_from_slice(b"\r\n");
215        }
216
217        if let Some(b) = body
218            && !b.is_empty()
219        {
220            req.extend_from_slice(b"content-length: ");
221            req.extend_from_slice(b.len().to_string().as_bytes());
222            req.extend_from_slice(b"\r\n");
223        }
224
225        req.extend_from_slice(b"\r\n");
226
227        if let Some(b) = body
228            && !b.is_empty()
229        {
230            req.extend_from_slice(b);
231        }
232
233        self.conn.send_nowait(&req)?;
234
235        // Parse response headers.
236        let mut status: u16 = 0;
237        let mut headers: Vec<(String, String)> = Vec::new();
238        let mut content_length: Option<usize> = None;
239        let mut chunked = false;
240        let mut content_encoding: Option<String> = None;
241        let mut headers_done = false;
242        let mut body_leftover = BytesMut::new();
243        let mut parse_error = false;
244
245        while !headers_done {
246            let n = self
247                .conn
248                .with_data(|data| {
249                    if let Some(end) = find_header_end(data) {
250                        let header_bytes = &data[..end];
251                        if let Some(parsed) = parse_response_headers(header_bytes) {
252                            status = parsed.status;
253                            headers = parsed.headers;
254                            content_length = parsed.content_length;
255                            chunked = parsed.chunked;
256                            content_encoding = parsed.content_encoding;
257                            headers_done = true;
258                            let header_consumed = end + 4;
259
260                            let remaining = &data[header_consumed..];
261                            if !remaining.is_empty() {
262                                body_leftover.extend_from_slice(remaining);
263                            }
264                        } else {
265                            parse_error = true;
266                        }
267                        ParseResult::Consumed(data.len())
268                    } else {
269                        ParseResult::Consumed(0)
270                    }
271                })
272                .await;
273
274            if n == 0 {
275                return Err(HttpError::ConnectionClosed);
276            }
277            if parse_error {
278                return Err(HttpError::Parse);
279            }
280        }
281
282        Ok(H1HeaderResult {
283            status,
284            headers,
285            content_length,
286            chunked,
287            content_encoding,
288            body_leftover,
289        })
290    }
291}
292
293/// Result of parsing HTTP/1.1 response headers.
294struct H1HeaderResult {
295    status: u16,
296    headers: Vec<(String, String)>,
297    content_length: Option<usize>,
298    chunked: bool,
299    #[cfg_attr(
300        not(any(feature = "gzip", feature = "zstd", feature = "brotli")),
301        allow(dead_code)
302    )]
303    content_encoding: Option<String>,
304    body_leftover: BytesMut,
305}
306
307/// Internal state for H1 streaming body reads.
308enum H1StreamState {
309    ContentLength {
310        remaining: usize,
311        leftover: BytesMut,
312    },
313    Chunked {
314        leftover: Vec<u8>,
315    },
316    Done,
317}
318
319/// Streaming HTTP/1.1 response. Borrows the connection exclusively.
320///
321/// Body chunks are yielded one at a time via [`next_chunk()`](Self::next_chunk).
322pub struct H1StreamingResponse<'a> {
323    conn: &'a mut ConnCtx,
324    status: u16,
325    headers: Vec<(String, String)>,
326    state: H1StreamState,
327}
328
329impl<'a> H1StreamingResponse<'a> {
330    /// HTTP status code.
331    pub fn status(&self) -> u16 {
332        self.status
333    }
334
335    /// Response headers as (name, value) pairs.
336    pub fn headers(&self) -> &[(String, String)] {
337        &self.headers
338    }
339
340    /// Get the first header value matching `name` (case-insensitive).
341    pub fn header(&self, name: &str) -> Option<&str> {
342        let lower = name.to_ascii_lowercase();
343        self.headers
344            .iter()
345            .find(|(k, _)| k.to_ascii_lowercase() == lower)
346            .map(|(_, v)| v.as_str())
347    }
348
349    /// Yield the next body chunk, or `None` when the body is complete.
350    pub async fn next_chunk(&mut self) -> Result<Option<Bytes>, HttpError> {
351        loop {
352            match &mut self.state {
353                H1StreamState::ContentLength {
354                    remaining,
355                    leftover,
356                } => {
357                    // Yield leftover first.
358                    if !leftover.is_empty() {
359                        let chunk = leftover.split().freeze();
360                        *remaining = remaining.saturating_sub(chunk.len());
361                        return Ok(Some(chunk));
362                    }
363                    if *remaining == 0 {
364                        self.state = H1StreamState::Done;
365                        return Ok(None);
366                    }
367                    // Read more from wire.
368                    let rem = *remaining;
369                    let mut got = BytesMut::new();
370                    let n = self
371                        .conn
372                        .with_data(|data| {
373                            let take = data.len().min(rem);
374                            got.extend_from_slice(&data[..take]);
375                            ParseResult::Consumed(take)
376                        })
377                        .await;
378                    if n == 0 {
379                        self.state = H1StreamState::Done;
380                        return Err(HttpError::ConnectionClosed);
381                    }
382                    *remaining -= got.len();
383                    return Ok(Some(got.freeze()));
384                }
385                H1StreamState::Chunked { leftover } => {
386                    match decode_chunk(leftover) {
387                        ChunkResult::Complete {
388                            data,
389                            consumed,
390                            is_last,
391                        } => {
392                            let chunk = Bytes::copy_from_slice(data);
393                            *leftover = leftover[consumed..].to_vec();
394                            if is_last {
395                                self.state = H1StreamState::Done;
396                                if chunk.is_empty() {
397                                    return Ok(None);
398                                }
399                                return Ok(Some(chunk));
400                            }
401                            return Ok(Some(chunk));
402                        }
403                        ChunkResult::NeedMore => {
404                            // Read more data.
405                            let n = self
406                                .conn
407                                .with_data(|data| {
408                                    leftover.extend_from_slice(data);
409                                    ParseResult::Consumed(data.len())
410                                })
411                                .await;
412                            if n == 0 {
413                                self.state = H1StreamState::Done;
414                                return Err(HttpError::ConnectionClosed);
415                            }
416                            // Loop back to try decoding again.
417                        }
418                    }
419                }
420                H1StreamState::Done => return Ok(None),
421            }
422        }
423    }
424}
425
426/// Find the position of `\r\n\r\n` in data, returns index of the first `\r`.
427fn find_header_end(data: &[u8]) -> Option<usize> {
428    (0..data.len().saturating_sub(3)).find(|&i| {
429        data[i] == b'\r' && data[i + 1] == b'\n' && data[i + 2] == b'\r' && data[i + 3] == b'\n'
430    })
431}
432
433struct ParsedHeaders {
434    status: u16,
435    headers: Vec<(String, String)>,
436    content_length: Option<usize>,
437    chunked: bool,
438    content_encoding: Option<String>,
439}
440
441/// Parse HTTP/1.1 response headers (everything before `\r\n\r\n`).
442fn parse_response_headers(data: &[u8]) -> Option<ParsedHeaders> {
443    let text = std::str::from_utf8(data).ok()?;
444    let mut lines = text.split("\r\n");
445
446    // Status line: HTTP/1.1 200 OK
447    let status_line = lines.next()?;
448    let mut parts = status_line.splitn(3, ' ');
449    let _version = parts.next()?;
450    let status_str = parts.next()?;
451    let status: u16 = status_str.parse().ok()?;
452
453    let mut headers = Vec::new();
454    let mut content_length = None;
455    let mut chunked = false;
456    let mut content_encoding = None;
457
458    for line in lines {
459        if line.is_empty() {
460            break;
461        }
462        if let Some((name, value)) = line.split_once(':') {
463            let name = name.trim().to_string();
464            let value = value.trim().to_string();
465
466            if name.eq_ignore_ascii_case("content-length") {
467                content_length = value.parse().ok();
468            }
469            if name.eq_ignore_ascii_case("transfer-encoding")
470                && value.eq_ignore_ascii_case("chunked")
471            {
472                chunked = true;
473            }
474            if name.eq_ignore_ascii_case("content-encoding") {
475                content_encoding = Some(value.clone());
476            }
477
478            headers.push((name, value));
479        }
480    }
481
482    Some(ParsedHeaders {
483        status,
484        headers,
485        content_length,
486        chunked,
487        content_encoding,
488    })
489}
490
491enum ChunkResult<'a> {
492    Complete {
493        data: &'a [u8],
494        consumed: usize,
495        is_last: bool,
496    },
497    NeedMore,
498}
499
500/// Decode one chunk from chunked transfer encoding.
501fn decode_chunk(data: &[u8]) -> ChunkResult<'_> {
502    // Find the chunk size line: <hex>\r\n
503    let crlf = match find_crlf(data) {
504        Some(pos) => pos,
505        None => return ChunkResult::NeedMore,
506    };
507
508    let size_str = match std::str::from_utf8(&data[..crlf]) {
509        Ok(s) => s.trim(),
510        Err(_) => return ChunkResult::NeedMore,
511    };
512
513    // Strip chunk extensions (;key=value).
514    let size_hex = size_str.split(';').next().unwrap_or("").trim();
515
516    let size = match usize::from_str_radix(size_hex, 16) {
517        Ok(s) => s,
518        Err(_) => return ChunkResult::NeedMore,
519    };
520
521    if size == 0 {
522        // Last chunk: 0\r\n followed by optional trailer headers and
523        // a final \r\n. Scan past any trailers to find the empty line.
524        let after_zero = crlf + 2;
525        let mut pos = after_zero;
526        loop {
527            match find_crlf(&data[pos..]) {
528                Some(0) => {
529                    // Empty line — end of trailer section.
530                    return ChunkResult::Complete {
531                        data: &[],
532                        consumed: pos + 2,
533                        is_last: true,
534                    };
535                }
536                Some(next_crlf) => {
537                    // Trailer header line — skip it.
538                    pos += next_crlf + 2;
539                }
540                None => return ChunkResult::NeedMore,
541            }
542        }
543    }
544
545    let chunk_start = crlf + 2;
546    let chunk_end = chunk_start + size;
547    let total = chunk_end + 2; // trailing \r\n
548
549    if data.len() < total {
550        return ChunkResult::NeedMore;
551    }
552
553    ChunkResult::Complete {
554        data: &data[chunk_start..chunk_end],
555        consumed: total,
556        is_last: false,
557    }
558}
559
560fn find_crlf(data: &[u8]) -> Option<usize> {
561    (0..data.len().saturating_sub(1)).find(|&i| data[i] == b'\r' && data[i + 1] == b'\n')
562}
563
564#[cfg(test)]
565mod tests {
566    use super::*;
567
568    #[test]
569    fn parse_simple_response() {
570        let data = b"HTTP/1.1 200 OK\r\ncontent-length: 5\r\n";
571        let parsed = parse_response_headers(data).unwrap();
572        assert_eq!(parsed.status, 200);
573        assert_eq!(parsed.content_length, Some(5));
574        assert!(!parsed.chunked);
575        assert_eq!(parsed.headers.len(), 1);
576    }
577
578    #[test]
579    fn parse_chunked_response() {
580        let data = b"HTTP/1.1 200 OK\r\ntransfer-encoding: chunked\r\n";
581        let parsed = parse_response_headers(data).unwrap();
582        assert_eq!(parsed.status, 200);
583        assert!(parsed.chunked);
584        assert_eq!(parsed.content_length, None);
585    }
586
587    #[test]
588    fn find_header_end_found() {
589        let data = b"HTTP/1.1 200 OK\r\ncontent-length: 0\r\n\r\nbody";
590        assert_eq!(find_header_end(data), Some(34));
591    }
592
593    #[test]
594    fn find_header_end_not_found() {
595        let data = b"HTTP/1.1 200 OK\r\ncontent-length: 0\r\n";
596        assert_eq!(find_header_end(data), None);
597    }
598
599    #[test]
600    fn decode_chunk_simple() {
601        let data = b"5\r\nhello\r\n";
602        match decode_chunk(data) {
603            ChunkResult::Complete {
604                data,
605                consumed,
606                is_last,
607            } => {
608                assert_eq!(data, b"hello");
609                assert_eq!(consumed, 10);
610                assert!(!is_last);
611            }
612            ChunkResult::NeedMore => panic!("expected Complete"),
613        }
614    }
615
616    #[test]
617    fn decode_chunk_last_with_empty_trailers() {
618        // Terminal chunk: "0\r\n\r\n" (no trailers, just the empty line).
619        let data = b"0\r\n\r\n";
620        match decode_chunk(data) {
621            ChunkResult::Complete {
622                is_last, consumed, ..
623            } => {
624                assert!(is_last);
625                assert_eq!(consumed, 5, "should consume 0\\r\\n\\r\\n");
626            }
627            ChunkResult::NeedMore => panic!("expected Complete"),
628        }
629    }
630
631    #[test]
632    fn decode_chunk_last_needs_trailing_crlf() {
633        // Just "0\r\n" without the trailing \r\n — need more data.
634        let data = b"0\r\n";
635        match decode_chunk(data) {
636            ChunkResult::NeedMore => {}
637            ChunkResult::Complete { .. } => panic!("expected NeedMore"),
638        }
639    }
640
641    #[test]
642    fn decode_chunk_last_with_trailers() {
643        // Terminal chunk with trailer headers.
644        let data = b"0\r\nTrailer: value\r\n\r\n";
645        match decode_chunk(data) {
646            ChunkResult::Complete {
647                is_last, consumed, ..
648            } => {
649                assert!(is_last);
650                assert_eq!(
651                    consumed,
652                    data.len(),
653                    "should consume entire trailer section"
654                );
655            }
656            ChunkResult::NeedMore => panic!("expected Complete"),
657        }
658    }
659
660    #[test]
661    fn decode_chunk_last_does_not_consume_next_response() {
662        // Terminal chunk followed by the start of the next response.
663        let data = b"0\r\n\r\nHTTP/1.1 200 OK\r\n";
664        match decode_chunk(data) {
665            ChunkResult::Complete {
666                is_last, consumed, ..
667            } => {
668                assert!(is_last);
669                assert_eq!(consumed, 5, "should only consume 0\\r\\n\\r\\n");
670                assert_eq!(&data[consumed..], b"HTTP/1.1 200 OK\r\n");
671            }
672            ChunkResult::NeedMore => panic!("expected Complete"),
673        }
674    }
675
676    #[test]
677    fn decode_chunk_need_more() {
678        let data = b"5\r\nhel";
679        match decode_chunk(data) {
680            ChunkResult::NeedMore => {}
681            ChunkResult::Complete { .. } => panic!("expected NeedMore"),
682        }
683    }
684
685    #[test]
686    fn parse_malformed_status_line_returns_none() {
687        // Missing status code.
688        assert!(parse_response_headers(b"HTTP/1.1 \r\n").is_none());
689        // Completely garbled.
690        assert!(parse_response_headers(b"NOT-HTTP\r\n").is_none());
691        // Empty.
692        assert!(parse_response_headers(b"").is_none());
693        // Just the version with no space.
694        assert!(parse_response_headers(b"HTTP/1.1\r\n").is_none());
695    }
696}