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