Skip to main content

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}