Skip to main content

http_wire/
response.rs

1//! HTTP response encoding and decoding.
2//!
3//! This module handles the serialization of `http::Response` objects into wire-format bytes
4//! and the parsing of raw bytes to determine response boundaries.
5//!
6//! # Response Encoding
7//!
8//! The [`WireEncode`] trait is implemented for [`http::Response`],
9//! allowing you to serialize responses to bytes via direct serialization
10//! (no async runtime, no HTTP pipeline, no `Clone` constraint on the body).
11//!
12//! ```rust
13//! use http_wire::WireEncode;
14//! use http::Response;
15//! use http_body_util::Full;
16//! use bytes::Bytes;
17//!
18//! let response = Response::new(Full::new(Bytes::from("Hello")));
19//! let wire_bytes = response.encode().unwrap();
20//! ```
21//!
22//! # Response Decoding
23//!
24//! Use [`FullResponse`] to decode HTTP responses from raw bytes.
25
26use bytes::{Bytes, BytesMut};
27
28pub use httparse::{Header, Response};
29
30use crate::error::WireError;
31use crate::util::{chunked_body_len, is_chunked_slice, version_to_str};
32use crate::{WireDecode, WireEncode};
33
34impl<B> WireEncode for http::Response<B>
35where
36    B: http_body_util::BodyExt,
37    B::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
38{
39    fn encode(self) -> Result<Bytes, WireError> {
40        let version = self.version();
41        if version != http::Version::HTTP_11 && version != http::Version::HTTP_10 {
42            return Err(WireError::UnsupportedVersion);
43        }
44
45        let (parts, body) = self.into_parts();
46        let status = parts.status;
47        let reason = status.canonical_reason().unwrap_or("Unknown");
48
49        // Collect body bytes synchronously.
50        // Works for any body type whose future completes without an async I/O driver
51        // (e.g. `Full<Bytes>`, `Empty<Bytes>`).
52        let body_bytes = futures::executor::block_on(body.collect())
53            .map_err(|e| WireError::Collection(e.into()))?
54            .to_bytes();
55
56        // Pre-allocate a rough estimate: 48 bytes per header covers name + value
57        // in the typical case; 16 bytes of fixed overhead absorbs the status line,
58        // separators and the blank line. Both are intentionally coarse — the point
59        // is to avoid the most common reallocations, not to be exact.
60        let mut buf = BytesMut::with_capacity(
61            parts.headers.len() * 48 + body_bytes.len() + 16,
62        );
63
64        // Status line: HTTP-version SP status-code SP reason-phrase CRLF
65        buf.extend_from_slice(version_to_str(version).as_bytes());
66        buf.extend_from_slice(b" ");
67        buf.extend_from_slice(status.as_str().as_bytes());
68        buf.extend_from_slice(b" ");
69        buf.extend_from_slice(reason.as_bytes());
70        buf.extend_from_slice(b"\r\n");
71
72        // Header fields
73        for (name, value) in &parts.headers {
74            buf.extend_from_slice(name.as_str().as_bytes());
75            buf.extend_from_slice(b": ");
76            buf.extend_from_slice(value.as_bytes());
77            buf.extend_from_slice(b"\r\n");
78        }
79
80        // Header/body separator
81        buf.extend_from_slice(b"\r\n");
82
83        // Body
84        buf.extend_from_slice(&body_bytes);
85
86        Ok(buf.freeze())
87    }
88}
89
90/// Decoder for extracting HTTP response status code and message length.
91///
92/// Returns `(StatusCode, usize)` containing the status code and total length in bytes
93/// of a complete HTTP response (headers + body), or `None` if incomplete or malformed.
94///
95/// Correctly handles status codes without bodies (1xx, 204, 304), `Content-Length`,
96/// and `Transfer-Encoding: chunked`.
97///
98/// # Example
99///
100/// ```rust
101/// use http_wire::WireDecode;
102/// use http_wire::response::FullResponse;
103///
104/// let raw = b"HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello";
105/// let mut headers = [httparse::EMPTY_HEADER; 16];
106/// let (full_response, length) = FullResponse::decode(raw, &mut headers).unwrap();
107/// assert_eq!(full_response.head.code, Some(200));
108/// assert_eq!(length, raw.len());
109/// ```
110pub struct FullResponse<'headers, 'buf> {
111    /// The parsed HTTP response headers and status line.
112    ///
113    /// Contains the HTTP version, status code, reason phrase, and headers.
114    /// Use `head.code` to access the status code, `head.reason` for the reason phrase,
115    /// and `head.headers` to iterate over the headers.
116    pub head: httparse::Response<'headers, 'buf>,
117    /// The response body as a byte slice.
118    ///
119    /// This is a reference into the original buffer passed to [`parse`](Self::parse)
120    /// or [`decode`](WireDecode::decode). It contains the complete body content
121    /// after decoding any transfer encodings (chunked or content-length).
122    pub body: &'buf [u8],
123}
124
125impl<'headers, 'buf> FullResponse<'headers, 'buf> {
126    /// Parse an HTTP response from raw bytes.
127    ///
128    /// This method parses the HTTP response from the provided buffer, extracting
129    /// the status code, headers, and body. It correctly handles responses with
130    /// no body (1xx, 204, 304), `Content-Length` specified bodies, and
131    /// `Transfer-Encoding: chunked` bodies.
132    ///
133    /// # Arguments
134    ///
135    /// * `buf` - The buffer containing the raw HTTP response bytes
136    ///
137    /// # Returns
138    ///
139    /// Returns `Ok(total_len)` where `total_len` is the complete message length (headers + body).
140    ///
141    /// # Errors
142    ///
143    /// Returns [`WireError::PartialHead`] if headers are incomplete,
144    /// [`WireError::HttparseError`] if header parsing fails,
145    /// [`WireError::InvalidChunkedBody`] if chunked encoding is malformed,
146    /// or [`WireError::IncompleteBody`] if the body is shorter than specified by `Content-Length`.
147    pub fn parse(&mut self, buf: &'buf [u8]) -> Result<usize, WireError> {
148        match self.head.parse(buf) {
149            Ok(httparse::Status::Complete(headers_len)) => {
150                let code = self.head.code.unwrap_or(200);
151
152                // Fast path for responses that never have a body (1xx, 204, 304)
153                if code == 204 || code == 304 || (100..200).contains(&code) {
154                    self.body = &[];
155                    return Ok(headers_len);
156                }
157
158                let mut content_len: Option<usize> = None;
159                let mut is_chunked = false;
160
161                // Scan headers for Content-Length or Transfer-Encoding
162                for header in self.head.headers.iter() {
163                    let name = header.name.as_bytes();
164                    if name.len() == 14 && name.eq_ignore_ascii_case(b"Content-Length") {
165                        content_len = std::str::from_utf8(header.value)
166                            .ok()
167                            .and_then(|s| s.parse().ok());
168                    } else if name.len() == 17 && name.eq_ignore_ascii_case(b"Transfer-Encoding") {
169                        is_chunked = is_chunked_slice(header.value);
170                    }
171                }
172
173                // Calculate body length
174                if is_chunked {
175                    let body_len = chunked_body_len(&buf[headers_len..])
176                        .ok_or(WireError::InvalidChunkedBody)?;
177                    self.body = &buf[headers_len..headers_len + body_len];
178                    Ok(headers_len + body_len)
179                } else {
180                    // If content-length is missing, length is 0
181                    let body_len = content_len.unwrap_or(0);
182                    let total = headers_len + body_len;
183                    if buf.len() >= total {
184                        self.body = &buf[headers_len..total];
185                        Ok(total)
186                    } else {
187                        Err(WireError::IncompleteBody(total - buf.len()))
188                    }
189                }
190            }
191            Ok(httparse::Status::Partial) => Err(WireError::PartialHead),
192            Err(err) => Err(err.into()),
193        }
194    }
195}
196
197impl<'headers, 'buf> WireDecode<'headers, 'buf> for FullResponse<'headers, 'buf> {
198    fn decode(
199        buf: &'buf [u8],
200        headers: &'headers mut [Header<'buf>],
201    ) -> Result<(Self, usize), WireError> {
202        let mut full_response = FullResponse {
203            head: httparse::Response::new(headers),
204            body: &[],
205        };
206
207        let total = full_response.parse(buf)?;
208        Ok((full_response, total))
209    }
210
211    // decode_uninit is not implemented for FullResponse because httparse::Response
212    // does not provide parse_with_uninit_headers method.
213    // The default implementation will panic with an appropriate message.
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219    use bytes::Bytes;
220    use http_body_util::{Empty, Full};
221
222    // ── Encoding ────────────────────────────────────────────────────────────
223
224    #[test]
225    fn test_response_encode_200() {
226        let response = http::Response::builder()
227            .status(200)
228            .header("Content-Type", "text/plain")
229            .body(Full::new(Bytes::from("Hello")))
230            .unwrap();
231
232        let bytes = response.encode().unwrap();
233        let output = String::from_utf8_lossy(&bytes);
234
235        assert!(output.starts_with("HTTP/1.1 200 OK\r\n"));
236        assert!(output.contains("content-type: text/plain\r\n"));
237        assert!(output.contains("\r\n\r\n"));
238        assert!(output.ends_with("Hello"));
239    }
240
241    #[test]
242    fn test_response_encode_404() {
243        let response = http::Response::builder()
244            .status(404)
245            .body(Full::new(Bytes::from("Not Found")))
246            .unwrap();
247
248        let bytes = response.encode().unwrap();
249        let output = String::from_utf8_lossy(&bytes);
250
251        assert!(output.starts_with("HTTP/1.1 404 Not Found\r\n"));
252        assert!(output.ends_with("Not Found"));
253    }
254
255    #[test]
256    fn test_response_encode_no_body() {
257        let response = http::Response::builder()
258            .status(204)
259            .header("Server", "http_wire")
260            .body(Empty::<Bytes>::new())
261            .unwrap();
262
263        let bytes = response.encode().unwrap();
264        let output = String::from_utf8_lossy(&bytes);
265
266        assert!(output.starts_with("HTTP/1.1 204 No Content\r\n"));
267        assert!(output.contains("server: http_wire\r\n"));
268        // Body must be empty after the separator
269        let parts: Vec<&str> = output.splitn(2, "\r\n\r\n").collect();
270        assert_eq!(parts.len(), 2);
271        assert!(parts[1].is_empty());
272    }
273
274    #[test]
275    fn test_response_encode_http10() {
276        let response = http::Response::builder()
277            .status(200)
278            .version(http::Version::HTTP_10)
279            .body(Empty::<Bytes>::new())
280            .unwrap();
281
282        let bytes = response.encode().unwrap();
283        let output = String::from_utf8_lossy(&bytes);
284
285        assert!(output.starts_with("HTTP/1.0 200 OK\r\n"));
286    }
287
288    #[test]
289    fn test_response_encode_http2_rejected() {
290        let response = http::Response::builder()
291            .status(200)
292            .version(http::Version::HTTP_2)
293            .body(Full::new(Bytes::from("Hello")))
294            .unwrap();
295
296        let result = response.encode();
297        assert!(matches!(result, Err(WireError::UnsupportedVersion)));
298    }
299
300    #[test]
301    fn test_response_encode_header_body_separator() {
302        let body = "Hello World";
303        let response = http::Response::builder()
304            .status(200)
305            .header("Content-Type", "text/plain")
306            .body(Full::new(Bytes::from(body)))
307            .unwrap();
308
309        let bytes = response.encode().unwrap();
310        let output = String::from_utf8_lossy(&bytes);
311
312        // Headers and body must be separated by exactly \r\n\r\n
313        assert!(output.contains("\r\n\r\n"));
314        let parts: Vec<&str> = output.splitn(2, "\r\n\r\n").collect();
315        assert_eq!(parts.len(), 2, "response must have a headers section and a body section");
316        assert_eq!(parts[1], body, "body must appear verbatim after the separator");
317    }
318
319    #[test]
320    fn test_response_encode_multiple_headers() {
321        let response = http::Response::builder()
322            .status(200)
323            .header("Content-Type", "application/json")
324            .header("X-Request-Id", "abc-123")
325            .header("Cache-Control", "no-cache")
326            .body(Empty::<Bytes>::new())
327            .unwrap();
328
329        let bytes = response.encode().unwrap();
330        let output = String::from_utf8_lossy(&bytes);
331
332        assert!(output.contains("content-type: application/json\r\n"));
333        assert!(output.contains("x-request-id: abc-123\r\n"));
334        assert!(output.contains("cache-control: no-cache\r\n"));
335    }
336
337    // ── Decoding ─────────────────────────────────────────────────────────────
338
339    #[test]
340    fn test_decode_response_no_body() {
341        let raw = b"HTTP/1.1 204 No Content\r\nServer: test\r\n\r\n";
342        let mut headers = [httparse::EMPTY_HEADER; 16];
343        let result = FullResponse::decode(raw, &mut headers);
344        assert!(result.is_ok());
345        let (response, len) = result.unwrap();
346        assert_eq!(response.head.code, Some(204));
347        assert_eq!(len, raw.len());
348        assert_eq!(response.body.len(), 0);
349    }
350
351    #[test]
352    fn test_decode_response_with_content_length() {
353        let raw = b"HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello";
354        let mut headers = [httparse::EMPTY_HEADER; 16];
355        let result = FullResponse::decode(raw, &mut headers);
356        assert!(result.is_ok());
357        let (response, len) = result.unwrap();
358        assert_eq!(response.head.code, Some(200));
359        assert_eq!(len, raw.len());
360        assert_eq!(response.body, b"hello");
361    }
362
363    #[test]
364    fn test_decode_response_incomplete_body() {
365        // Content-Length says 10, but body is only 5 bytes
366        let raw = b"HTTP/1.1 200 OK\r\nContent-Length: 10\r\n\r\nhello";
367        let mut headers = [httparse::EMPTY_HEADER; 16];
368        let result = FullResponse::decode(raw, &mut headers);
369        assert!(matches!(result, Err(WireError::IncompleteBody(_))));
370    }
371
372    #[test]
373    fn test_decode_response_incomplete_headers() {
374        let raw = b"HTTP/1.1 200 OK\r\nContent-Length: 5\r\n";
375        let mut headers = [httparse::EMPTY_HEADER; 16];
376        let result = FullResponse::decode(raw, &mut headers);
377        assert!(matches!(result, Err(WireError::PartialHead)));
378    }
379
380    #[test]
381    fn test_decode_response_chunked_encoding() {
382        let raw = b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nhello\r\n0\r\n\r\n";
383        let mut headers = [httparse::EMPTY_HEADER; 16];
384        let result = FullResponse::decode(raw, &mut headers);
385        assert!(result.is_ok());
386        let (response, len) = result.unwrap();
387        assert_eq!(response.head.code, Some(200));
388        assert_eq!(len, raw.len());
389    }
390
391    #[test]
392    fn test_decode_response_chunked_multiple_chunks() {
393        let raw = b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nhello\r\n6\r\n world\r\n0\r\n\r\n";
394        let mut headers = [httparse::EMPTY_HEADER; 16];
395        let result = FullResponse::decode(raw, &mut headers);
396        assert!(result.is_ok());
397        let (response, len) = result.unwrap();
398        assert_eq!(response.head.code, Some(200));
399        assert_eq!(len, raw.len());
400    }
401
402    #[test]
403    fn test_decode_response_chunked_incomplete() {
404        // Missing final 0\r\n\r\n
405        let raw = b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nhello\r\n";
406        let mut headers = [httparse::EMPTY_HEADER; 16];
407        let result = FullResponse::decode(raw, &mut headers);
408        assert!(matches!(result, Err(WireError::InvalidChunkedBody)));
409    }
410
411    #[test]
412    fn test_decode_response_extra_data_after() {
413        // Buffer has extra data after the response — decode should return correct length
414        let response = b"HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello";
415        let mut raw = response.to_vec();
416        raw.extend_from_slice(b"extra garbage data");
417        let mut headers = [httparse::EMPTY_HEADER; 16];
418        let (_, len) = FullResponse::decode(&raw, &mut headers).unwrap();
419        assert_eq!(len, response.len());
420    }
421
422    #[test]
423    fn test_decode_response_chunked_case_insensitive() {
424        let raw = b"HTTP/1.1 200 OK\r\nTransfer-Encoding: CHUNKED\r\n\r\n5\r\nhello\r\n0\r\n\r\n";
425        let mut headers = [httparse::EMPTY_HEADER; 16];
426        let result = FullResponse::decode(raw, &mut headers);
427        assert!(result.is_ok());
428        let (response, len) = result.unwrap();
429        assert_eq!(response.head.code, Some(200));
430        assert_eq!(len, raw.len());
431    }
432
433    #[test]
434    fn test_decode_response_304_no_body() {
435        // 304 responses never have a body
436        let raw = b"HTTP/1.1 304 Not Modified\r\nETag: \"abc\"\r\n\r\n";
437        let mut headers = [httparse::EMPTY_HEADER; 16];
438        let result = FullResponse::decode(raw, &mut headers);
439        assert!(result.is_ok());
440        let (response, len) = result.unwrap();
441        assert_eq!(response.head.code, Some(304));
442        assert_eq!(len, raw.len());
443        assert_eq!(response.body.len(), 0);
444    }
445
446    #[test]
447    fn test_decode_response_1xx_no_body() {
448        // 1xx responses never have a body
449        let raw = b"HTTP/1.1 100 Continue\r\n\r\n";
450        let mut headers = [httparse::EMPTY_HEADER; 16];
451        let result = FullResponse::decode(raw, &mut headers);
452        assert!(result.is_ok());
453        let (response, len) = result.unwrap();
454        assert_eq!(response.head.code, Some(100));
455        assert_eq!(len, raw.len());
456        assert_eq!(response.body.len(), 0);
457    }
458
459    #[test]
460    fn test_decode_response_fields_access() {
461        let raw =
462            b"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 11\r\n\r\nHello World";
463        let mut headers = [httparse::EMPTY_HEADER; 16];
464        let (response, total_len) = FullResponse::decode(raw, &mut headers).unwrap();
465
466        assert_eq!(response.head.code, Some(200));
467        assert_eq!(response.head.reason, Some("OK"));
468        assert_eq!(response.head.version, Some(1));
469
470        assert_eq!(response.head.headers.len(), 2);
471        assert_eq!(response.head.headers[0].name, "Content-Type");
472        assert_eq!(response.head.headers[0].value, b"text/plain");
473        assert_eq!(response.head.headers[1].name, "Content-Length");
474        assert_eq!(response.head.headers[1].value, b"11");
475
476        assert_eq!(response.body, b"Hello World");
477        assert_eq!(total_len, raw.len());
478    }
479
480    #[test]
481    #[should_panic(
482        expected = "decode_uninit is not available for this type due to missing parse_with_uninit_headers method"
483    )]
484    fn test_decode_response_uninit_panics() {
485        let raw = b"HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello";
486        let mut headers = [const { std::mem::MaybeUninit::uninit() }; 16];
487        let _result = FullResponse::decode_uninit(raw, &mut headers);
488    }
489}