sendra_core/http/response.rs
1//! [`Response`] and [`RedirectHop`]: what sending a [`crate::Request`] gets
2//! back.
3
4use std::time::Duration;
5
6/// The result of sending a [`crate::Request`].
7///
8/// Headers are a `Vec` of pairs rather than a map: HTTP allows repeats
9/// (`set-cookie`) and wire order is worth preserving for display.
10#[derive(Debug, Clone)]
11pub struct Response {
12 pub status: u16,
13 pub status_text: String,
14 pub headers: Vec<(String, String)>,
15 /// The response body, decoded from the bytes on the wire **lossily**:
16 /// any byte sequence that is not valid UTF-8 is replaced with U+FFFD
17 /// (`\u{fffd}`, the replacement character) rather than erroring.
18 ///
19 /// This is a deliberate contract, not an accident of the type. A body can
20 /// legitimately be a PNG or a protobuf, and a tool whose job is to show
21 /// you what came back should show you *something* rather than refuse the
22 /// whole response over its encoding — the status, the headers and the
23 /// elapsed time are all still true and all still worth seeing. So an
24 /// invalid body is never an error.
25 ///
26 /// The cost is that it is **not round-trippable**: `body.as_bytes()` is
27 /// not what the server sent, and the original bytes cannot be recovered
28 /// from here. Everything downstream that reads this — assertions,
29 /// captures, scripts, `--json` output — is reading the replaced text, so
30 /// a `body_contains` against a binary payload is comparing against U+FFFD
31 /// and will not match. Binary-safe bodies (keeping the raw bytes
32 /// alongside, and telling the user when a substitution happened) are a
33 /// later concern; today the substitution is silent.
34 pub body: String,
35 pub elapsed: Duration,
36 /// Every redirect hop that led to this response, oldest first: empty when
37 /// the request was answered directly, when [`FollowRedirects::Disabled`](crate::config::FollowRedirects::Disabled)
38 /// left a 3xx response as this one, or when only one hop's worth of
39 /// following happened and it landed here without an intermediate stop.
40 ///
41 /// Each entry is the status of the response that redirected, and the
42 /// `Location` it pointed at (resolved to an absolute URL) — the same two
43 /// facts a `curl -v` trace would show for that hop. This response's own
44 /// status and headers are not repeated here.
45 pub redirects: Vec<RedirectHop>,
46}
47
48impl Response {
49 pub fn is_success(&self) -> bool {
50 (200..300).contains(&self.status)
51 }
52}
53
54/// One hop of a redirect chain, recorded on the way to a [`Response`].
55#[derive(Debug, Clone, PartialEq, Eq)]
56pub struct RedirectHop {
57 /// The status of the response that redirected — `301`, `302`, and so on.
58 pub status: u16,
59 /// Where it pointed: the `Location` header, resolved against the URL that
60 /// received it, as an absolute URL.
61 pub location: String,
62}