Skip to main content

nexus_web/http/
response.rs

1use super::error::HttpError;
2use nexus_net::buf::ReadBuf;
3
4/// A parsed HTTP/1.x response. Borrows from the reader's buffer.
5pub struct Response<'a> {
6    /// HTTP status code (e.g., 101, 200, 404).
7    pub status: u16,
8    /// Reason phrase (e.g., "Switching Protocols", "OK").
9    pub reason: &'a str,
10    /// HTTP version (0 = HTTP/1.0, 1 = HTTP/1.1).
11    pub version: u8,
12    data: &'a [u8],
13    header_offsets: &'a [(usize, usize, usize, usize)],
14}
15
16impl<'a> Response<'a> {
17    /// Look up a header value by name (case-insensitive).
18    ///
19    /// Returns `None` if the header is not found or if the value is not valid UTF-8.
20    /// Use [`header_bytes`](Self::header_bytes) for raw access to non-UTF-8 values.
21    pub fn header(&self, name: &str) -> Option<&'a str> {
22        for &(ns, nl, vs, vl) in self.header_offsets {
23            let hname = &self.data[ns..ns + nl];
24            if hname.eq_ignore_ascii_case(name.as_bytes()) {
25                return std::str::from_utf8(&self.data[vs..vs + vl]).ok();
26            }
27        }
28        None
29    }
30
31    /// Look up a raw header value by name (case-insensitive).
32    ///
33    /// Returns the value as raw bytes without UTF-8 validation.
34    pub fn header_bytes(&self, name: &str) -> Option<&'a [u8]> {
35        for &(ns, nl, vs, vl) in self.header_offsets {
36            let hname = &self.data[ns..ns + nl];
37            if hname.eq_ignore_ascii_case(name.as_bytes()) {
38                return Some(&self.data[vs..vs + vl]);
39            }
40        }
41        None
42    }
43
44    /// Iterate over headers as (name, value) pairs.
45    ///
46    /// Skips headers with non-UTF-8 names or values.
47    /// Use [`header_count`](Self::header_count) for the total count including non-UTF-8.
48    pub fn headers(&self) -> impl Iterator<Item = (&'a str, &'a str)> {
49        self.header_offsets.iter().filter_map(|&(ns, nl, vs, vl)| {
50            let name = std::str::from_utf8(&self.data[ns..ns + nl]).ok()?;
51            let value = std::str::from_utf8(&self.data[vs..vs + vl]).ok()?;
52            Some((name, value))
53        })
54    }
55
56    /// Number of parsed headers (including non-UTF-8).
57    pub fn header_count(&self) -> usize {
58        self.header_offsets.len()
59    }
60}
61
62impl std::fmt::Debug for Response<'_> {
63    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64        f.debug_struct("Response")
65            .field("status", &self.status)
66            .field("reason", &self.reason)
67            .field("version", &self.version)
68            .field("headers", &self.header_count())
69            .finish()
70    }
71}
72
73/// Sans-IO HTTP/1.x response parser.
74///
75/// # Usage
76///
77/// ```
78/// use nexus_web::http::ResponseReader;
79///
80/// let mut reader = ResponseReader::new(4096);
81/// reader.read(b"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\n\r\n").unwrap();
82/// let resp = reader.next().unwrap().unwrap();
83/// assert_eq!(resp.status, 101);
84/// assert_eq!(resp.header("Upgrade"), Some("websocket"));
85/// ```
86pub struct ResponseReader {
87    buf: ReadBuf,
88    max_headers: usize,
89    max_head_size: usize,
90    max_body_size: usize,
91    head_len: Option<usize>,
92    header_offsets: Vec<(usize, usize, usize, usize)>,
93    status: u16,
94    reason_start: usize,
95    reason_end: usize,
96    version: u8,
97    // Cached during try_parse to avoid post-parse header scans.
98    cached_content_length: Option<Result<usize, ()>>,
99    cached_is_chunked: bool,
100    /// Raw wire bytes consumed for the last response body.
101    /// Used by `consume_response` to advance past the correct number of bytes.
102    last_raw_body_bytes: usize,
103}
104
105impl ResponseReader {
106    /// Create with the given buffer capacity.
107    #[must_use]
108    pub fn new(capacity: usize) -> Self {
109        Self {
110            buf: ReadBuf::with_capacity(capacity),
111            max_headers: 64,
112            max_head_size: 8192,
113            max_body_size: 0,
114            head_len: None,
115            header_offsets: Vec::with_capacity(16),
116            status: 0,
117            reason_start: 0,
118            reason_end: 0,
119            version: 1,
120            cached_content_length: None,
121            cached_is_chunked: false,
122            last_raw_body_bytes: 0,
123        }
124    }
125
126    /// Set maximum number of headers. Default: 64.
127    #[must_use]
128    pub fn max_headers(mut self, n: usize) -> Self {
129        self.max_headers = n;
130        self
131    }
132
133    /// Set maximum head size. Default: 8KB.
134    #[must_use]
135    pub fn max_head_size(mut self, n: usize) -> Self {
136        self.max_head_size = n;
137        self
138    }
139
140    /// Set maximum response body size. Default: 0 (no limit).
141    ///
142    /// When set, responses with Content-Length exceeding this value
143    /// will be rejected during validation.
144    #[must_use]
145    pub fn max_body_size(mut self, n: usize) -> Self {
146        self.max_body_size = n;
147        self
148    }
149
150    /// Configured maximum body size (0 = no limit).
151    pub fn max_body_size_limit(&self) -> usize {
152        self.max_body_size
153    }
154
155    /// Buffer wire bytes.
156    pub fn read(&mut self, src: &[u8]) -> Result<(), HttpError> {
157        let spare = self.buf.spare();
158        if src.len() > spare.len() {
159            self.buf.compact();
160            let spare = self.buf.spare();
161            if src.len() > spare.len() {
162                return Err(HttpError::BufferFull {
163                    needed: src.len(),
164                    available: spare.len(),
165                });
166            }
167        }
168        let spare = self.buf.spare();
169        spare[..src.len()].copy_from_slice(src);
170        self.buf.filled(src.len());
171        Ok(())
172    }
173
174    /// Writable region for direct in-buffer writes. Pair with
175    /// [`filled()`](Self::filled) to commit bytes after the write.
176    /// Used by [`crate::WireStream`] to deliver bytes from a transport
177    /// without a slice intermediate.
178    #[inline]
179    pub fn spare(&mut self) -> &mut [u8] {
180        self.buf.spare()
181    }
182
183    /// Commit `n` bytes written into [`spare()`](Self::spare).
184    #[inline]
185    pub fn filled(&mut self, n: usize) {
186        self.buf.filled(n);
187    }
188
189    /// Read bytes from a source directly into the internal buffer.
190    ///
191    /// Returns bytes read, or 0 on EOF.
192    pub fn read_from<R: std::io::Read>(&mut self, src: &mut R) -> std::io::Result<usize> {
193        let spare = self.buf.spare();
194        if spare.is_empty() {
195            self.buf.compact();
196        }
197        let spare = self.buf.spare();
198        if spare.is_empty() {
199            return Err(std::io::Error::other("response buffer full"));
200        }
201        let n = src.read(spare)?;
202        self.buf.filled(n);
203        Ok(n)
204    }
205
206    /// Bytes of data buffered beyond the parsed headers (body bytes).
207    /// Available after `next()` returns `Some`.
208    pub fn body_remaining(&self) -> usize {
209        self.head_len
210            .map_or(0, |n| self.buf.data().len().saturating_sub(n))
211    }
212
213    /// Look up a parsed response header by name (case-insensitive).
214    ///
215    /// Returns `None` if headers haven't been parsed yet or the header
216    /// is not found. Only valid after `next()` returns `Some`.
217    pub fn header(&self, name: &str) -> Option<&str> {
218        self.head_len?;
219        let data = self.buf.data();
220        for &(ns, nl, vs, vl) in &self.header_offsets {
221            if data[ns..ns + nl].eq_ignore_ascii_case(name.as_bytes()) {
222                return std::str::from_utf8(&data[vs..vs + vl]).ok();
223            }
224        }
225        None
226    }
227
228    /// Parse the next response.
229    #[allow(clippy::should_implement_trait)]
230    pub fn next(&mut self) -> Result<Option<Response<'_>>, HttpError> {
231        if self.head_len.is_none() {
232            self.try_parse()?;
233        }
234
235        if self.head_len.is_none() {
236            return Ok(None);
237        }
238
239        let data = self.buf.data();
240        if self.reason_end > data.len() || self.reason_start > self.reason_end {
241            return Err(HttpError::Malformed("reason phrase out of bounds"));
242        }
243        let reason = std::str::from_utf8(&data[self.reason_start..self.reason_end])
244            .map_err(|_| HttpError::Malformed("invalid UTF-8 in reason phrase"))?;
245
246        Ok(Some(Response {
247            status: self.status,
248            reason,
249            version: self.version,
250            data,
251            header_offsets: &self.header_offsets,
252        }))
253    }
254
255    /// Bytes after the parsed head.
256    pub fn remainder(&self) -> &[u8] {
257        match self.head_len {
258            Some(n) => &self.buf.data()[n..],
259            None => &[],
260        }
261    }
262
263    /// HTTP status code from parsed headers.
264    pub fn status(&self) -> u16 {
265        self.status
266    }
267
268    /// Number of parsed headers.
269    pub fn header_count(&self) -> usize {
270        self.header_offsets.len()
271    }
272
273    /// Cached Content-Length from parsed headers.
274    /// `None` = header absent, `Some(Ok(n))` = valid, `Some(Err(()))` = present but malformed.
275    pub fn content_length(&self) -> Option<Result<usize, ()>> {
276        self.cached_content_length
277    }
278
279    /// Whether Transfer-Encoding includes "chunked" (cached from parse).
280    pub fn is_chunked(&self) -> bool {
281        self.cached_is_chunked
282    }
283
284    /// Set the raw wire bytes consumed for the response body.
285    ///
286    /// For Content-Length responses: equals Content-Length.
287    /// For chunked responses: includes chunk framing overhead.
288    /// For bodyless (1xx/204/304): 0.
289    ///
290    /// Must be called before `consume_response()`.
291    pub fn set_body_consumed(&mut self, raw_bytes: usize) {
292        self.last_raw_body_bytes = raw_bytes;
293    }
294
295    /// Advance past a consumed response, preserving any pipelined bytes.
296    ///
297    /// Uses `last_raw_body_bytes` (set via [`set_body_consumed`](Self::set_body_consumed)) to
298    /// determine how many wire bytes to skip. Call before parsing the
299    /// next response on a keep-alive connection.
300    pub fn consume_response(&mut self) {
301        if let Some(head_len) = self.head_len {
302            let consumed = head_len + self.last_raw_body_bytes;
303            if consumed <= self.buf.data().len() {
304                self.buf.advance(consumed);
305            } else {
306                self.buf.clear();
307            }
308        }
309        self.head_len = None;
310        self.header_offsets.clear();
311        self.cached_content_length = None;
312        self.cached_is_chunked = false;
313        self.last_raw_body_bytes = 0;
314    }
315
316    /// Reset for a new response. Discards all buffered data.
317    pub fn reset(&mut self) {
318        self.buf.clear();
319        self.head_len = None;
320        self.header_offsets.clear();
321        self.cached_content_length = None;
322        self.cached_is_chunked = false;
323        self.last_raw_body_bytes = 0;
324    }
325
326    fn try_parse(&mut self) -> Result<(), HttpError> {
327        let data = self.buf.data();
328        if data.is_empty() {
329            return Ok(());
330        }
331        if data.len() > self.max_head_size {
332            return Err(HttpError::HeadTooLarge {
333                max: self.max_head_size,
334            });
335        }
336
337        let mut stack_headers = [httparse::EMPTY_HEADER; 64];
338        let mut heap_headers;
339        let headers: &mut [httparse::Header<'_>] = if self.max_headers <= 64 {
340            &mut stack_headers[..self.max_headers]
341        } else {
342            heap_headers = vec![httparse::EMPTY_HEADER; self.max_headers];
343            &mut heap_headers
344        };
345        let mut resp = httparse::Response::new(headers);
346
347        match resp.parse(data) {
348            Ok(httparse::Status::Complete(head_len)) => {
349                let status = resp
350                    .code
351                    .ok_or(HttpError::Malformed("missing status code"))?;
352                let reason = resp
353                    .reason
354                    .ok_or(HttpError::Malformed("missing reason phrase"))?;
355                let version = resp
356                    .version
357                    .ok_or(HttpError::Malformed("missing HTTP version"))?;
358
359                let data_ptr = data.as_ptr();
360                self.status = status;
361                // SAFETY: reason ptr is within data (same allocation).
362                self.reason_start = unsafe { reason.as_ptr().offset_from(data_ptr) } as usize;
363                self.reason_end = self.reason_start + reason.len();
364                self.version = version;
365
366                self.header_offsets.clear();
367                self.cached_content_length = None;
368                self.cached_is_chunked = false;
369
370                for h in resp.headers.iter() {
371                    // SAFETY: header name/value pointers are within data (same allocation).
372                    let ns = unsafe { h.name.as_ptr().offset_from(data_ptr) } as usize;
373                    let nl = h.name.len();
374                    // SAFETY: header value pointer is within data (same allocation as data_ptr).
375                    let vs = unsafe { h.value.as_ptr().offset_from(data_ptr) } as usize;
376                    let vl = h.value.len();
377                    debug_assert!(ns + nl <= data.len(), "header name offset out of bounds");
378                    debug_assert!(vs + vl <= data.len(), "header value offset out of bounds");
379                    self.header_offsets.push((ns, nl, vs, vl));
380
381                    // Cache Content-Length and Transfer-Encoding during parse
382                    // to avoid post-parse linear scans.
383                    if h.name.eq_ignore_ascii_case("Content-Length") {
384                        self.cached_content_length = Some(
385                            std::str::from_utf8(h.value)
386                                .ok()
387                                .and_then(|v| v.trim().parse::<usize>().ok())
388                                .ok_or(()),
389                        );
390                    } else if h.name.eq_ignore_ascii_case("Transfer-Encoding")
391                        && let Ok(te) = std::str::from_utf8(h.value)
392                    {
393                        self.cached_is_chunked = te
394                            .split(',')
395                            .any(|t| t.trim().eq_ignore_ascii_case("chunked"));
396                    }
397                }
398
399                self.head_len = Some(head_len);
400                Ok(())
401            }
402            Ok(httparse::Status::Partial) => Ok(()),
403            Err(httparse::Error::TooManyHeaders) => Err(HttpError::TooManyHeaders),
404            Err(_) => Err(HttpError::Malformed("httparse rejected response")),
405        }
406    }
407}
408
409/// Lets a [`WireStream`](crate::WireStream) feed bytes directly into
410/// the ResponseReader's spare region — one fewer copy than going
411/// through a slice intermediary.
412impl crate::ParserSink for ResponseReader {
413    #[inline]
414    fn spare(&mut self) -> &mut [u8] {
415        ResponseReader::spare(self)
416    }
417
418    #[inline]
419    fn filled(&mut self, n: usize) {
420        ResponseReader::filled(self, n);
421    }
422}
423
424/// Validate that a header name or value contains no CRLF characters.
425fn validate_header_value(s: &str) -> Result<(), super::error::HttpError> {
426    if s.bytes().any(|b| b == b'\r' || b == b'\n') {
427        return Err(super::error::HttpError::InvalidHeaderValue);
428    }
429    Ok(())
430}
431
432fn copy_to(dst: &mut [u8], offset: usize, src: &[u8]) -> Result<usize, super::error::HttpError> {
433    let end = offset + src.len();
434    if end > dst.len() {
435        return Err(super::error::HttpError::BufferTooSmall {
436            needed: end,
437            available: dst.len(),
438        });
439    }
440    dst[offset..end].copy_from_slice(src);
441    Ok(src.len())
442}
443
444fn write_u16(dst: &mut [u8], offset: usize, val: u16) -> Result<usize, super::error::HttpError> {
445    debug_assert!(
446        val >= 100 && val <= 999,
447        "HTTP status must be 3 digits: {val}"
448    );
449    if offset + 3 > dst.len() {
450        return Err(super::error::HttpError::BufferTooSmall {
451            needed: offset + 3,
452            available: dst.len(),
453        });
454    }
455    dst[offset] = (val / 100) as u8 + b'0';
456    dst[offset + 1] = ((val / 10) % 10) as u8 + b'0';
457    dst[offset + 2] = (val % 10) as u8 + b'0';
458    Ok(3)
459}
460
461/// Compute the exact size needed for a request.
462#[must_use]
463pub fn request_size(method: &str, path: &str, headers: &[(&str, &str)]) -> usize {
464    let mut size = method.len() + 1 + path.len() + 11; // " HTTP/1.1\r\n"
465    for &(name, value) in headers {
466        size += name.len() + 2 + value.len() + 2; // ": " + "\r\n"
467    }
468    size + 2 // final "\r\n"
469}
470
471/// Compute the exact size needed for a response.
472#[must_use]
473pub fn response_size(reason: &str, headers: &[(&str, &str)]) -> usize {
474    let mut size = 9 + 3 + 1 + reason.len() + 2; // "HTTP/1.1 " + status + " " + reason + "\r\n"
475    for &(name, value) in headers {
476        size += name.len() + 2 + value.len() + 2;
477    }
478    size + 2
479}
480
481/// Write an HTTP/1.1 request into a byte buffer. Returns bytes written.
482///
483/// Returns `HttpError::BufferTooSmall` if `dst` is undersized.
484/// Use [`request_size`] to compute the exact size needed.
485pub fn write_request(
486    method: &str,
487    path: &str,
488    headers: &[(&str, &str)],
489    dst: &mut [u8],
490) -> Result<usize, super::error::HttpError> {
491    let mut offset = 0;
492    offset += copy_to(dst, offset, method.as_bytes())?;
493    offset += copy_to(dst, offset, b" ")?;
494    offset += copy_to(dst, offset, path.as_bytes())?;
495    offset += copy_to(dst, offset, b" HTTP/1.1\r\n")?;
496    for &(name, value) in headers {
497        validate_header_value(name)?;
498        validate_header_value(value)?;
499        offset += copy_to(dst, offset, name.as_bytes())?;
500        offset += copy_to(dst, offset, b": ")?;
501        offset += copy_to(dst, offset, value.as_bytes())?;
502        offset += copy_to(dst, offset, b"\r\n")?;
503    }
504    offset += copy_to(dst, offset, b"\r\n")?;
505    Ok(offset)
506}
507
508/// Write an HTTP/1.1 response into a byte buffer. Returns bytes written.
509///
510/// Returns `HttpError::BufferTooSmall` if `dst` is undersized.
511/// Use [`response_size`] to compute the exact size needed.
512pub fn write_response(
513    status: u16,
514    reason: &str,
515    headers: &[(&str, &str)],
516    dst: &mut [u8],
517) -> Result<usize, super::error::HttpError> {
518    let mut offset = 0;
519    offset += copy_to(dst, offset, b"HTTP/1.1 ")?;
520    offset += write_u16(dst, offset, status)?;
521    offset += copy_to(dst, offset, b" ")?;
522    offset += copy_to(dst, offset, reason.as_bytes())?;
523    offset += copy_to(dst, offset, b"\r\n")?;
524    for &(name, value) in headers {
525        validate_header_value(name)?;
526        validate_header_value(value)?;
527        offset += copy_to(dst, offset, name.as_bytes())?;
528        offset += copy_to(dst, offset, b": ")?;
529        offset += copy_to(dst, offset, value.as_bytes())?;
530        offset += copy_to(dst, offset, b"\r\n")?;
531    }
532    offset += copy_to(dst, offset, b"\r\n")?;
533    Ok(offset)
534}
535
536#[cfg(test)]
537mod tests {
538    use super::*;
539
540    #[test]
541    fn basic_101_response() {
542        let mut r = ResponseReader::new(4096);
543        r.read(b"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n\r\n").unwrap();
544        let resp = r.next().unwrap().unwrap();
545        assert_eq!(resp.status, 101);
546        assert_eq!(resp.reason, "Switching Protocols");
547        assert_eq!(resp.version, 1);
548        assert_eq!(resp.header("Upgrade"), Some("websocket"));
549        assert_eq!(resp.header("Connection"), Some("Upgrade"));
550    }
551
552    #[test]
553    fn basic_200_response() {
554        let mut r = ResponseReader::new(4096);
555        r.read(b"HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nHello")
556            .unwrap();
557        let resp = r.next().unwrap().unwrap();
558        assert_eq!(resp.status, 200);
559        assert_eq!(resp.reason, "OK");
560        assert_eq!(resp.header("Content-Length"), Some("5"));
561    }
562
563    #[test]
564    fn response_remainder() {
565        let mut r = ResponseReader::new(4096);
566        r.read(b"HTTP/1.1 200 OK\r\n\r\nbody data").unwrap();
567        let _resp = r.next().unwrap().unwrap();
568        assert_eq!(r.remainder(), b"body data");
569    }
570
571    #[test]
572    fn partial_response() {
573        let mut r = ResponseReader::new(4096);
574        r.read(b"HTTP/1.1 200 OK\r\nHost: ").unwrap();
575        assert!(r.next().unwrap().is_none());
576        r.read(b"example.com\r\n\r\n").unwrap();
577        let resp = r.next().unwrap().unwrap();
578        assert_eq!(resp.status, 200);
579        assert_eq!(resp.header("Host"), Some("example.com"));
580    }
581
582    #[test]
583    fn write_request_round_trip() {
584        use crate::http::RequestReader;
585        let mut dst = [0u8; 256];
586        let n = write_request(
587            "GET",
588            "/ws",
589            &[("Host", "localhost:8080"), ("Upgrade", "websocket")],
590            &mut dst,
591        )
592        .unwrap();
593
594        let mut r = RequestReader::new(4096);
595        r.read(&dst[..n]).unwrap();
596        let req = r.next().unwrap().unwrap();
597        assert_eq!(req.method, "GET");
598        assert_eq!(req.path, "/ws");
599        assert_eq!(req.header("Upgrade"), Some("websocket"));
600    }
601
602    #[test]
603    fn write_response_round_trip() {
604        let mut dst = [0u8; 256];
605        let n = write_response(
606            101,
607            "Switching Protocols",
608            &[("Upgrade", "websocket"), ("Connection", "Upgrade")],
609            &mut dst,
610        )
611        .unwrap();
612
613        let mut r = ResponseReader::new(4096);
614        r.read(&dst[..n]).unwrap();
615        let resp = r.next().unwrap().unwrap();
616        assert_eq!(resp.status, 101);
617        assert_eq!(resp.header("Connection"), Some("Upgrade"));
618    }
619
620    #[test]
621    fn reset_then_reuse() {
622        let mut r = ResponseReader::new(4096);
623        r.read(b"HTTP/1.1 200 OK\r\n\r\n").unwrap();
624        let _ = r.next().unwrap().unwrap();
625        r.reset();
626        r.read(b"HTTP/1.1 404 Not Found\r\n\r\n").unwrap();
627        let resp = r.next().unwrap().unwrap();
628        assert_eq!(resp.status, 404);
629    }
630}