Skip to main content

http_net/
message.rs

1//! Outgoing messages: the `Response` a server sends and the `Request` a
2//! client sends.
3//!
4//! Both builders validate as they go and remember the first problem instead
5//! of failing each call, so a message can be built in one chained
6//! expression; the problem surfaces as an error when the connection writes
7//! the message. Framing headers (`content-length`, `transfer-encoding`,
8//! `connection`, `upgrade`) are owned by the connection, which derives them
9//! from the body and the exchange; setting them by hand is refused, which
10//! makes conflicting framing on the way out impossible.
11
12use alloc::{borrow::Cow, format, string::String, vec::Vec};
13use core::fmt;
14
15use crate::{
16    error::HttpError,
17    head::{check_target, is_h2c},
18    method::Method,
19    redact::{SerializedFields, serialized_fields},
20    syntax::{check_field_value, is_protocol, is_token, trim_ows},
21};
22
23/// How an outgoing message's body is supplied.
24#[derive(Clone, Debug, PartialEq, Eq)]
25pub(crate) enum OutBody {
26    /// The whole body, sent with `content-length`.
27    Full(Cow<'static, [u8]>),
28    /// Streamed afterwards with exactly this many bytes.
29    Length(u64),
30    /// Streamed afterwards with chunked encoding.
31    Chunked,
32}
33
34/// Validates a field and appends `name: value\r\n`.
35fn push_field(
36    fields: &mut Vec<u8>,
37    name: &str,
38    value: &[u8],
39    reserved: &[&str],
40) -> Result<(), HttpError> {
41    if !is_token(name.as_bytes()) {
42        return Err(HttpError::InvalidHeaderName);
43    }
44    if reserved.iter().any(|r| r.eq_ignore_ascii_case(name)) {
45        return Err(HttpError::ReservedHeader);
46    }
47    if trim_ows(value).len() != value.len() {
48        // Surrounding whitespace would be stripped by the recipient; refusing
49        // it keeps what is sent equal to what is received.
50        return Err(HttpError::InvalidHeaderValue);
51    }
52    check_field_value(value)?;
53    fields.extend_from_slice(name.as_bytes());
54    fields.extend_from_slice(b": ");
55    fields.extend_from_slice(value);
56    fields.extend_from_slice(b"\r\n");
57    Ok(())
58}
59
60const RESPONSE_RESERVED: &[&str] = &[
61    "content-length",
62    "transfer-encoding",
63    "connection",
64    "upgrade",
65];
66const REQUEST_RESERVED: &[&str] = &[
67    "content-length",
68    "transfer-encoding",
69    "connection",
70    "upgrade",
71];
72
73/// A response for a [`ServerConn`](crate::ServerConn) to send.
74///
75/// The status line, `content-length` / `transfer-encoding`, and
76/// `connection` are written by the connection, which knows the request's
77/// method and version: a `HEAD` request gets the headers without the body,
78/// an HTTP/1.0 client never gets chunked encoding, and `connection: close`
79/// is added whenever the connection will close.
80///
81/// # Examples
82///
83/// ```
84/// use http_net::Response;
85///
86/// let ok = Response::text(200, "hello");
87/// assert_eq!(ok.status(), 200);
88///
89/// let asset = Response::new(200)
90///     .header("content-type", "text/css")
91///     .header("cache-control", "max-age=31536000, immutable")
92///     .body(&b"body { margin: 0 }"[..]);
93/// assert_eq!(asset.header_value("content-type"), Some(&b"text/css"[..]));
94/// ```
95#[derive(Clone, PartialEq, Eq)]
96pub struct Response {
97    status: u16,
98    fields: Vec<u8>,
99    pub(crate) body: OutBody,
100    pub(crate) close: bool,
101    pub(crate) upgrade: bool,
102    pub(crate) has_date: bool,
103    error: Option<HttpError>,
104}
105
106impl Response {
107    /// A response with this status, no extra headers, and an empty body.
108    ///
109    /// The status is validated when the response is written (`100..=599`,
110    /// and only `101` among the 1xx codes).
111    ///
112    /// # Examples
113    ///
114    /// ```
115    /// let r = http_net::Response::new(204);
116    /// assert_eq!(r.status(), 204);
117    /// ```
118    #[must_use]
119    pub fn new(status: u16) -> Self {
120        Self {
121            status,
122            fields: Vec::new(),
123            body: OutBody::Full(Cow::Borrowed(&[])),
124            close: false,
125            upgrade: false,
126            has_date: false,
127            error: None,
128        }
129    }
130
131    /// A `text/plain; charset=utf-8` response with `text` as its body.
132    ///
133    /// # Examples
134    ///
135    /// ```
136    /// let r = http_net::Response::text(404, "no such page");
137    /// assert_eq!(r.body_bytes(), Some(&b"no such page"[..]));
138    /// ```
139    #[must_use]
140    pub fn text(status: u16, text: &str) -> Self {
141        Self::new(status)
142            .header("content-type", "text/plain; charset=utf-8")
143            .body(text.as_bytes().to_vec())
144    }
145
146    /// The response a server sends after [`ServerConn::poll`](crate::ServerConn::poll)
147    /// returned a parse error: the status from [`HttpError::status`] (400
148    /// when it has none), a short `text/plain` body with the error's
149    /// one-line description, and [`close`](Self::close). The text is static
150    /// and never includes the bytes that were refused.
151    ///
152    /// # Examples
153    ///
154    /// ```
155    /// use http_net::{HttpError, Response, ServerConn};
156    ///
157    /// let r = Response::from_error(&HttpError::HeadersTooLarge);
158    /// assert_eq!(r.status(), 431);
159    /// assert!(r.closes());
160    ///
161    /// let mut conn = ServerConn::new();
162    /// let _ = conn.feed(b"GET / HTTP/1.1\r\n\r\n");
163    /// let err = conn.poll().unwrap_err();
164    /// let mut out = Vec::new();
165    /// conn.respond(&Response::from_error(&err), &mut out).unwrap();
166    /// assert!(out.starts_with(b"HTTP/1.1 400 Bad Request\r\n"));
167    /// ```
168    #[must_use]
169    pub fn from_error(error: &HttpError) -> Self {
170        Self::new(error.status().unwrap_or(400))
171            .header("content-type", "text/plain; charset=utf-8")
172            .body(format!("{error}").into_bytes())
173            .close()
174    }
175
176    /// A `101 Switching Protocols` response handing the connection to
177    /// `protocol` (for example `websocket`, or a `name/version` product
178    /// token). The connection writes `connection: upgrade` and
179    /// `upgrade: <protocol>`; add any protocol-specific headers (such as
180    /// `sec-websocket-accept`) with [`header`](Self::header).
181    ///
182    /// The protocol must be one the request offered (RFC 9110 section 7.8),
183    /// compared ASCII case-insensitively; a name without a version matches
184    /// an offer of that name with any version. Otherwise, and always for
185    /// `h2c` (RFC 9113 section 3.1), writing it fails with
186    /// [`HttpError::UpgradeNotOffered`].
187    ///
188    /// # Examples
189    ///
190    /// ```
191    /// let r = http_net::Response::switching_protocols("websocket")
192    ///     .header("sec-websocket-accept", "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=");
193    /// assert_eq!(r.status(), 101);
194    /// assert_eq!(r.header_value("upgrade"), Some(&b"websocket"[..]));
195    /// ```
196    #[must_use]
197    pub fn switching_protocols(protocol: &str) -> Self {
198        let mut r = Self::new(101);
199        r.upgrade = true;
200        if !is_protocol(protocol.as_bytes()) {
201            r.error = Some(HttpError::InvalidHeaderValue);
202            return r;
203        }
204        if is_h2c(protocol.as_bytes()) {
205            r.error = Some(HttpError::UpgradeNotOffered);
206            return r;
207        }
208        r.fields
209            .extend_from_slice(b"connection: upgrade\r\nupgrade: ");
210        r.fields.extend_from_slice(protocol.as_bytes());
211        r.fields.extend_from_slice(b"\r\n");
212        r
213    }
214
215    /// Adds a header field. The name must be a token and the value may not
216    /// contain control characters other than tab, nor leading or trailing
217    /// whitespace. Framing headers are refused with
218    /// [`HttpError::ReservedHeader`]; the first problem is reported when the
219    /// response is written.
220    ///
221    /// # Examples
222    ///
223    /// ```
224    /// use http_net::{HttpError, Response, ServerConn};
225    ///
226    /// let bad = Response::new(200).header("content-length", "5");
227    /// let mut conn = ServerConn::new();
228    /// let _ = conn.feed(b"GET / HTTP/1.1\r\nHost: a\r\n\r\n");
229    /// let _ = conn.poll();
230    /// assert_eq!(conn.respond(&bad, &mut Vec::new()), Err(HttpError::ReservedHeader));
231    /// ```
232    #[must_use]
233    pub fn header(mut self, name: &str, value: impl AsRef<[u8]>) -> Self {
234        if self.error.is_none() {
235            let value = value.as_ref();
236            if let Err(e) = push_field(&mut self.fields, name, value, RESPONSE_RESERVED) {
237                self.error = Some(e);
238            } else if name.eq_ignore_ascii_case("date") {
239                self.has_date = true;
240            }
241        }
242        self
243    }
244
245    /// Sets the whole body; it is sent with `content-length`. Static data
246    /// (an embedded asset) is borrowed rather than copied.
247    ///
248    /// # Examples
249    ///
250    /// ```
251    /// static PAGE: &[u8] = b"<!doctype html><title>x</title>";
252    /// let r = http_net::Response::new(200).body(PAGE);
253    /// assert_eq!(r.body_bytes(), Some(PAGE));
254    /// ```
255    #[must_use]
256    pub fn body(mut self, body: impl Into<Cow<'static, [u8]>>) -> Self {
257        self.body = OutBody::Full(body.into());
258        self
259    }
260
261    /// Streams the body with chunked encoding: after
262    /// [`ServerConn::respond`](crate::ServerConn::respond), send data with
263    /// [`ServerConn::send_body`](crate::ServerConn::send_body) and end it
264    /// with [`ServerConn::end_body`](crate::ServerConn::end_body). For an
265    /// HTTP/1.0 client the connection falls back to closing the connection
266    /// to end the body.
267    ///
268    /// # Examples
269    ///
270    /// ```
271    /// let r = http_net::Response::new(200).chunked();
272    /// assert_eq!(r.body_bytes(), None);
273    /// ```
274    #[must_use]
275    pub fn chunked(mut self) -> Self {
276        self.body = OutBody::Chunked;
277        self
278    }
279
280    /// Streams a body of exactly `len` bytes, sent with `content-length`;
281    /// sending more or fewer is [`HttpError::BodyLengthMismatch`].
282    ///
283    /// # Examples
284    ///
285    /// ```
286    /// let r = http_net::Response::new(200).content_length(1 << 20);
287    /// assert_eq!(r.body_bytes(), None);
288    /// ```
289    #[must_use]
290    pub fn content_length(mut self, len: u64) -> Self {
291        self.body = OutBody::Length(len);
292        self
293    }
294
295    /// Closes the connection after this response.
296    ///
297    /// # Examples
298    ///
299    /// ```
300    /// let r = http_net::Response::new(503).close();
301    /// assert!(r.closes());
302    /// ```
303    #[must_use]
304    pub fn close(mut self) -> Self {
305        self.close = true;
306        self
307    }
308
309    /// The status code.
310    ///
311    /// # Examples
312    ///
313    /// ```
314    /// assert_eq!(http_net::Response::new(418).status(), 418);
315    /// ```
316    #[must_use]
317    pub fn status(&self) -> u16 {
318        self.status
319    }
320
321    /// Whether [`close`](Self::close) was requested.
322    ///
323    /// # Examples
324    ///
325    /// ```
326    /// assert!(!http_net::Response::new(200).closes());
327    /// ```
328    #[must_use]
329    pub fn closes(&self) -> bool {
330        self.close
331    }
332
333    /// The full body, or `None` for a streamed one.
334    ///
335    /// # Examples
336    ///
337    /// ```
338    /// assert_eq!(http_net::Response::new(200).body_bytes(), Some(&[][..]));
339    /// ```
340    #[must_use]
341    pub fn body_bytes(&self) -> Option<&[u8]> {
342        match &self.body {
343            OutBody::Full(b) => Some(b),
344            _ => None,
345        }
346    }
347
348    /// The value of the first header named `name` added to this response
349    /// (ASCII case-insensitive), including the `upgrade` of
350    /// [`switching_protocols`](Self::switching_protocols).
351    ///
352    /// # Examples
353    ///
354    /// ```
355    /// let r = http_net::Response::new(200).header("x-id", "7");
356    /// assert_eq!(r.header_value("X-ID"), Some(&b"7"[..]));
357    /// ```
358    #[must_use]
359    pub fn header_value(&self, name: &str) -> Option<&[u8]> {
360        field_value(&self.fields, name)
361    }
362
363    pub(crate) fn error(&self) -> Option<HttpError> {
364        self.error
365    }
366
367    pub(crate) fn fields(&self) -> &[u8] {
368        &self.fields
369    }
370
371    /// The protocol a `101` built with
372    /// [`switching_protocols`](Self::switching_protocols) switches to.
373    pub(crate) fn upgrade_protocol(&self) -> Option<&[u8]> {
374        if self.upgrade {
375            self.header_value("upgrade")
376        } else {
377            None
378        }
379    }
380}
381
382/// Shows status, fields, body size, and flags; the values of credential
383/// fields (`set-cookie` above all) are replaced by `<redacted N bytes>`.
384impl fmt::Debug for Response {
385    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
386        f.debug_struct("Response")
387            .field("status", &self.status)
388            .field("fields", &SerializedFields(&self.fields))
389            .field("body", &body_debug(&self.body))
390            .field("close", &self.close)
391            .field("error", &self.error)
392            .finish()
393    }
394}
395
396/// A request for a [`ClientConn`](crate::ClientConn) to send.
397///
398/// HTTP/1.1 requires a `Host` field; the connection refuses to send a
399/// request without one. The request line always says HTTP/1.1.
400///
401/// # Examples
402///
403/// ```
404/// use http_net::Request;
405///
406/// let get = Request::get("/index.html").header("host", "example.com");
407/// let post = Request::post("/api")
408///     .header("host", "example.com")
409///     .header("content-type", "application/json")
410///     .body(br#"{"ok":true}"#.to_vec());
411/// assert_eq!(get.method(), "GET");
412/// assert_eq!(post.target(), "/api");
413/// ```
414#[derive(Clone, PartialEq, Eq)]
415pub struct Request {
416    method: Cow<'static, str>,
417    pub(crate) method_kind: Method,
418    target: String,
419    fields: Vec<u8>,
420    pub(crate) body: OutBody,
421    pub(crate) close: bool,
422    pub(crate) upgrade: bool,
423    pub(crate) has_host: bool,
424    error: Option<HttpError>,
425}
426
427impl Request {
428    /// A request with `method` and `target`, validated when it is sent:
429    /// the method must be a token and the target must be in a form the
430    /// method allows (`/path?query`, an absolute URI, `host:port` for
431    /// `CONNECT`, or `*` for `OPTIONS`).
432    ///
433    /// # Examples
434    ///
435    /// ```
436    /// let r = http_net::Request::new("PURGE", "/cache/a");
437    /// assert_eq!(r.method(), "PURGE");
438    /// ```
439    #[must_use]
440    pub fn new(method: &str, target: &str) -> Self {
441        let method_kind = Method::from_bytes(method.as_bytes());
442        let mut error = None;
443        if !is_token(method.as_bytes()) {
444            error = Some(HttpError::InvalidMethod);
445        } else if let Err(e) = check_target(method_kind, target.as_bytes()) {
446            error = Some(e);
447        }
448        Self {
449            method: match method_kind.as_str() {
450                Some(s) => Cow::Borrowed(s),
451                None => Cow::Owned(method.into()),
452            },
453            method_kind,
454            target: target.into(),
455            fields: Vec::new(),
456            body: OutBody::Full(Cow::Borrowed(&[])),
457            close: false,
458            upgrade: false,
459            has_host: false,
460            error,
461        }
462    }
463
464    /// A `GET` request.
465    ///
466    /// # Examples
467    ///
468    /// ```
469    /// assert_eq!(http_net::Request::get("/").method(), "GET");
470    /// ```
471    #[must_use]
472    pub fn get(target: &str) -> Self {
473        Self::new("GET", target)
474    }
475
476    /// A `POST` request.
477    ///
478    /// # Examples
479    ///
480    /// ```
481    /// assert_eq!(http_net::Request::post("/").method(), "POST");
482    /// ```
483    #[must_use]
484    pub fn post(target: &str) -> Self {
485        Self::new("POST", target)
486    }
487
488    /// A `HEAD` request: its response carries headers and no body.
489    ///
490    /// # Examples
491    ///
492    /// ```
493    /// assert_eq!(http_net::Request::head("/").method(), "HEAD");
494    /// ```
495    #[must_use]
496    pub fn head(target: &str) -> Self {
497        Self::new("HEAD", target)
498    }
499
500    /// A `PUT` request.
501    ///
502    /// # Examples
503    ///
504    /// ```
505    /// assert_eq!(http_net::Request::put("/files/a").method(), "PUT");
506    /// ```
507    #[must_use]
508    pub fn put(target: &str) -> Self {
509        Self::new("PUT", target)
510    }
511
512    /// A `DELETE` request.
513    ///
514    /// # Examples
515    ///
516    /// ```
517    /// assert_eq!(http_net::Request::delete("/files/a").method(), "DELETE");
518    /// ```
519    #[must_use]
520    pub fn delete(target: &str) -> Self {
521        Self::new("DELETE", target)
522    }
523
524    /// An `OPTIONS` request; `*` asks about the server as a whole.
525    ///
526    /// # Examples
527    ///
528    /// ```
529    /// assert_eq!(http_net::Request::options("*").target(), "*");
530    /// ```
531    #[must_use]
532    pub fn options(target: &str) -> Self {
533        Self::new("OPTIONS", target)
534    }
535
536    /// Adds a header field, validated as for [`Response::header`]. A second
537    /// `host` is refused with [`HttpError::InvalidHost`].
538    ///
539    /// # Examples
540    ///
541    /// ```
542    /// let r = http_net::Request::get("/").header("host", "a").header("accept", "*/*");
543    /// assert_eq!(r.header_value("accept"), Some(&b"*/*"[..]));
544    /// ```
545    #[must_use]
546    pub fn header(mut self, name: &str, value: impl AsRef<[u8]>) -> Self {
547        if self.error.is_none() {
548            let value = value.as_ref();
549            let is_host = name.eq_ignore_ascii_case("host");
550            if is_host && (self.has_host || !crate::syntax::is_host_value(value)) {
551                self.error = Some(HttpError::InvalidHost);
552            } else if let Err(e) = push_field(&mut self.fields, name, value, REQUEST_RESERVED) {
553                self.error = Some(e);
554            } else {
555                self.has_host |= is_host;
556            }
557        }
558        self
559    }
560
561    /// Sets the whole body, sent with `content-length`.
562    ///
563    /// # Examples
564    ///
565    /// ```
566    /// let r = http_net::Request::post("/").body(&b"x=1"[..]);
567    /// assert_eq!(r.body_bytes(), Some(&b"x=1"[..]));
568    /// ```
569    #[must_use]
570    pub fn body(mut self, body: impl Into<Cow<'static, [u8]>>) -> Self {
571        self.body = OutBody::Full(body.into());
572        self
573    }
574
575    /// Streams the body with chunked encoding through
576    /// [`ClientConn::send_body`](crate::ClientConn::send_body) and
577    /// [`ClientConn::end_body`](crate::ClientConn::end_body).
578    ///
579    /// # Examples
580    ///
581    /// ```
582    /// assert_eq!(http_net::Request::post("/").chunked().body_bytes(), None);
583    /// ```
584    #[must_use]
585    pub fn chunked(mut self) -> Self {
586        self.body = OutBody::Chunked;
587        self
588    }
589
590    /// Streams a body of exactly `len` bytes.
591    ///
592    /// # Examples
593    ///
594    /// ```
595    /// assert_eq!(http_net::Request::post("/").content_length(4).body_bytes(), None);
596    /// ```
597    #[must_use]
598    pub fn content_length(mut self, len: u64) -> Self {
599        self.body = OutBody::Length(len);
600        self
601    }
602
603    /// Asks the server to close the connection after its response.
604    ///
605    /// # Examples
606    ///
607    /// ```
608    /// let _ = http_net::Request::get("/").header("host", "a").close();
609    /// ```
610    #[must_use]
611    pub fn close(mut self) -> Self {
612        self.close = true;
613        self
614    }
615
616    /// Asks to switch the connection to `protocol` (writes
617    /// `connection: upgrade` and `upgrade: <protocol>`). A `101` answer
618    /// hands the connection over; see
619    /// [`ClientEvent::Upgraded`](crate::ClientEvent::Upgraded).
620    ///
621    /// `protocol` is a token or a `name/version` product token. `h2c` is
622    /// refused with [`HttpError::UpgradeNotOffered`]: RFC 9113 section 3.1
623    /// deprecates the HTTP/2 upgrade.
624    ///
625    /// # Examples
626    ///
627    /// ```
628    /// use http_net::{ClientConn, HttpError, Request};
629    ///
630    /// let r = Request::get("/rpc").header("host", "a").upgrade("websocket");
631    /// assert_eq!(r.header_value("upgrade"), Some(&b"websocket"[..]));
632    ///
633    /// let h2c = Request::get("/").header("host", "a").upgrade("h2c");
634    /// assert_eq!(
635    ///     ClientConn::new().send_request(&h2c, &mut Vec::new()),
636    ///     Err(HttpError::UpgradeNotOffered)
637    /// );
638    /// ```
639    #[must_use]
640    pub fn upgrade(mut self, protocol: &str) -> Self {
641        if self.error.is_none() {
642            if is_h2c(protocol.as_bytes()) {
643                self.error = Some(HttpError::UpgradeNotOffered);
644            } else if is_protocol(protocol.as_bytes()) {
645                self.upgrade = true;
646                self.fields
647                    .extend_from_slice(b"connection: upgrade\r\nupgrade: ");
648                self.fields.extend_from_slice(protocol.as_bytes());
649                self.fields.extend_from_slice(b"\r\n");
650            } else {
651                self.error = Some(HttpError::InvalidHeaderValue);
652            }
653        }
654        self
655    }
656
657    /// The method text.
658    ///
659    /// # Examples
660    ///
661    /// ```
662    /// assert_eq!(http_net::Request::new("OPTIONS", "*").method(), "OPTIONS");
663    /// ```
664    #[must_use]
665    pub fn method(&self) -> &str {
666        &self.method
667    }
668
669    /// The request target.
670    ///
671    /// # Examples
672    ///
673    /// ```
674    /// assert_eq!(http_net::Request::get("/a?b").target(), "/a?b");
675    /// ```
676    #[must_use]
677    pub fn target(&self) -> &str {
678        &self.target
679    }
680
681    /// The full body, or `None` for a streamed one.
682    ///
683    /// # Examples
684    ///
685    /// ```
686    /// assert_eq!(http_net::Request::get("/").body_bytes(), Some(&[][..]));
687    /// ```
688    #[must_use]
689    pub fn body_bytes(&self) -> Option<&[u8]> {
690        match &self.body {
691            OutBody::Full(b) => Some(b),
692            _ => None,
693        }
694    }
695
696    /// The value of the first header named `name` added to this request.
697    ///
698    /// # Examples
699    ///
700    /// ```
701    /// let r = http_net::Request::get("/").header("host", "a");
702    /// assert_eq!(r.header_value("HOST"), Some(&b"a"[..]));
703    /// ```
704    #[must_use]
705    pub fn header_value(&self, name: &str) -> Option<&[u8]> {
706        field_value(&self.fields, name)
707    }
708
709    pub(crate) fn error(&self) -> Option<HttpError> {
710        self.error
711    }
712
713    pub(crate) fn fields(&self) -> &[u8] {
714        &self.fields
715    }
716}
717
718/// Shows method, target, fields, and body size; the values of credential
719/// fields (`authorization`, `proxy-authorization`, `cookie`) are replaced by
720/// `<redacted N bytes>`.
721impl fmt::Debug for Request {
722    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
723        f.debug_struct("Request")
724            .field("method", &self.method)
725            .field("target", &self.target)
726            .field("fields", &SerializedFields(&self.fields))
727            .field("body", &body_debug(&self.body))
728            .field("error", &self.error)
729            .finish()
730    }
731}
732
733fn body_debug(body: &OutBody) -> String {
734    match body {
735        OutBody::Full(b) => format!("{} bytes", b.len()),
736        OutBody::Length(n) => format!("streamed, {n} bytes"),
737        OutBody::Chunked => "streamed, chunked".into(),
738    }
739}
740
741/// Looks a header up in serialized `name: value\r\n` lines.
742fn field_value<'a>(fields: &'a [u8], name: &str) -> Option<&'a [u8]> {
743    serialized_fields(fields)
744        .find(|(n, _)| n.eq_ignore_ascii_case(name.as_bytes()))
745        .map(|(_, v)| v)
746}
747
748#[cfg(test)]
749mod tests {
750    use alloc::string::ToString;
751
752    use super::*;
753
754    #[test]
755    fn test_response_refuses_reserved_and_invalid_headers() {
756        assert_eq!(
757            Response::new(200)
758                .header("Transfer-Encoding", "chunked")
759                .error(),
760            Some(HttpError::ReservedHeader)
761        );
762        assert_eq!(
763            Response::new(200).header("bad name", "x").error(),
764            Some(HttpError::InvalidHeaderName)
765        );
766        assert_eq!(
767            Response::new(200).header("x", "a\r\nInjected: 1").error(),
768            Some(HttpError::BareCr)
769        );
770        assert_eq!(
771            Response::new(200).header("x", " padded").error(),
772            Some(HttpError::InvalidHeaderValue)
773        );
774        // The first error sticks.
775        let r = Response::new(200)
776            .header("a b", "1")
777            .header("connection", "close");
778        assert_eq!(r.error(), Some(HttpError::InvalidHeaderName));
779    }
780
781    #[test]
782    fn test_request_validates_method_target_and_host() {
783        assert_eq!(
784            Request::new("G T", "/").error(),
785            Some(HttpError::InvalidMethod)
786        );
787        assert_eq!(
788            Request::get("no-slash").error(),
789            Some(HttpError::InvalidTarget)
790        );
791        assert_eq!(
792            Request::get("/")
793                .header("host", "a")
794                .header("Host", "b")
795                .error(),
796            Some(HttpError::InvalidHost)
797        );
798        assert_eq!(
799            Request::get("/").header("host", "a b").error(),
800            Some(HttpError::InvalidHost)
801        );
802        assert!(Request::get("/").header("host", "a").has_host);
803        assert_eq!(
804            Request::get("/").upgrade("a b").error(),
805            Some(HttpError::InvalidHeaderValue)
806        );
807    }
808
809    #[test]
810    fn test_from_error_uses_status_text_and_close() {
811        let r = Response::from_error(&HttpError::UnsupportedTransferEncoding);
812        assert_eq!(r.status(), 501);
813        assert!(r.closes());
814        assert_eq!(
815            r.header_value("content-type"),
816            Some(&b"text/plain; charset=utf-8"[..])
817        );
818        assert_eq!(
819            r.body_bytes(),
820            Some(
821                HttpError::UnsupportedTransferEncoding
822                    .to_string()
823                    .as_bytes()
824            )
825        );
826        // Errors without a request status still get a well-formed 400.
827        assert_eq!(
828            Response::from_error(&HttpError::UnexpectedEof).status(),
829            400
830        );
831        assert_eq!(
832            Response::from_error(&HttpError::InvalidState("x")).error(),
833            None
834        );
835    }
836
837    #[test]
838    fn test_request_shortcuts_set_the_method() {
839        for (r, m) in [
840            (Request::head("/"), "HEAD"),
841            (Request::put("/"), "PUT"),
842            (Request::delete("/"), "DELETE"),
843            (Request::options("*"), "OPTIONS"),
844        ] {
845            assert_eq!(r.method(), m);
846            assert_eq!(r.error(), None);
847        }
848        assert_eq!(Request::put("*").error(), Some(HttpError::InvalidTarget));
849        assert_eq!(Request::head("/").method_kind, Method::Head);
850    }
851
852    #[test]
853    fn test_upgrade_builders_accept_product_tokens_and_refuse_h2c() {
854        let r = Response::switching_protocols("foo/2");
855        assert_eq!(r.error(), None);
856        assert_eq!(r.upgrade_protocol(), Some(&b"foo/2"[..]));
857        assert_eq!(
858            Response::switching_protocols("h2c").error(),
859            Some(HttpError::UpgradeNotOffered)
860        );
861        assert_eq!(
862            Response::switching_protocols("H2C").error(),
863            Some(HttpError::UpgradeNotOffered)
864        );
865        assert_eq!(
866            Response::switching_protocols("foo/").error(),
867            Some(HttpError::InvalidHeaderValue)
868        );
869        assert_eq!(Response::new(200).upgrade_protocol(), None);
870        assert_eq!(
871            Request::get("/").upgrade("h2c").error(),
872            Some(HttpError::UpgradeNotOffered)
873        );
874        assert!(Request::get("/").upgrade("websocket/13").upgrade);
875    }
876
877    /// Threat T-35: credential values never reach `Debug` output.
878    #[test]
879    fn test_debug_redacts_credential_fields() {
880        let r = Response::new(200)
881            .header("Set-Cookie", "sid=s3cr3t; HttpOnly")
882            .header("x-trace", "visible");
883        let shown = alloc::format!("{r:?}");
884        assert!(!shown.contains("s3cr3t"), "{shown}");
885        assert!(shown.contains("<redacted 20 bytes>"), "{shown}");
886        assert!(shown.contains("visible"), "{shown}");
887        assert_eq!(
888            r.header_value("set-cookie"),
889            Some(&b"sid=s3cr3t; HttpOnly"[..])
890        );
891
892        let q = Request::get("/")
893            .header("host", "a")
894            .header("Authorization", "Bearer t0k3n")
895            .header("PROXY-AUTHORIZATION", "Basic cDpx")
896            .header("cookie", "a=b");
897        let shown = alloc::format!("{q:?}");
898        for secret in ["t0k3n", "cDpx", "a=b"] {
899            assert!(!shown.contains(secret), "{shown}");
900        }
901        assert!(shown.contains("\"host\", \"a\""), "{shown}");
902    }
903
904    #[test]
905    fn test_field_lookup_and_debug() {
906        let r = Response::switching_protocols("websocket").header("x", "");
907        assert_eq!(r.header_value("connection"), Some(&b"upgrade"[..]));
908        assert_eq!(r.header_value("x"), Some(&b""[..]));
909        assert!(alloc::format!("{r:?}").contains("Response"));
910        assert_eq!(
911            Response::switching_protocols("a b").error(),
912            Some(HttpError::InvalidHeaderValue)
913        );
914    }
915}