halldyll_core/fetch/
response.rs

1//! Response - HTTP response handling
2
3use bytes::Bytes;
4use reqwest::header::HeaderMap;
5use reqwest::StatusCode;
6use std::collections::HashMap;
7use url::Url;
8
9use crate::types::provenance::{RedirectHop, RequestTimings};
10
11/// HTTP response with metadata
12#[derive(Debug)]
13pub struct FetchResponse {
14    /// Final URL after redirects
15    pub final_url: Url,
16    /// HTTP status code
17    pub status: StatusCode,
18    /// Response headers
19    pub headers: HeaderMap,
20    /// Response body (raw bytes)
21    pub body: Bytes,
22    /// Redirect chain
23    pub redirect_chain: Vec<RedirectHop>,
24    /// Request timings
25    pub timings: RequestTimings,
26    /// Size before decompression
27    pub compressed_size: Option<u64>,
28    /// Is this a 304 Not Modified response?
29    pub not_modified: bool,
30}
31
32impl FetchResponse {
33    /// Creates a new response
34    pub fn new(final_url: Url, status: StatusCode, headers: HeaderMap, body: Bytes) -> Self {
35        Self {
36            final_url,
37            status,
38            headers,
39            body,
40            redirect_chain: Vec::new(),
41            timings: RequestTimings::default(),
42            compressed_size: None,
43            not_modified: status == StatusCode::NOT_MODIFIED,
44        }
45    }
46
47    /// Response Content-Type
48    pub fn content_type(&self) -> Option<String> {
49        self.headers
50            .get("content-type")
51            .and_then(|v| v.to_str().ok())
52            .map(|s| s.split(';').next().unwrap_or(s).trim().to_lowercase())
53    }
54
55    /// Response charset
56    pub fn charset(&self) -> Option<String> {
57        self.headers
58            .get("content-type")
59            .and_then(|v| v.to_str().ok())
60            .and_then(|ct| {
61                ct.split(';')
62                    .find(|part| part.trim().to_lowercase().starts_with("charset="))
63                    .map(|part| part.split('=').nth(1).unwrap_or("utf-8").trim().to_lowercase())
64            })
65    }
66
67    /// Response ETag
68    pub fn etag(&self) -> Option<String> {
69        self.headers
70            .get("etag")
71            .and_then(|v| v.to_str().ok())
72            .map(String::from)
73    }
74
75    /// Response Last-Modified
76    pub fn last_modified(&self) -> Option<String> {
77        self.headers
78            .get("last-modified")
79            .and_then(|v| v.to_str().ok())
80            .map(String::from)
81    }
82
83    /// Response Cache-Control
84    pub fn cache_control(&self) -> Option<String> {
85        self.headers
86            .get("cache-control")
87            .and_then(|v| v.to_str().ok())
88            .map(String::from)
89    }
90
91    /// X-Robots-Tag header
92    pub fn x_robots_tag(&self) -> Option<String> {
93        self.headers
94            .get("x-robots-tag")
95            .and_then(|v| v.to_str().ok())
96            .map(String::from)
97    }
98
99    /// Headers as HashMap
100    pub fn headers_map(&self) -> HashMap<String, String> {
101        self.headers
102            .iter()
103            .filter_map(|(name, value)| {
104                value.to_str().ok().map(|v| (name.to_string(), v.to_string()))
105            })
106            .collect()
107    }
108
109    /// Body size
110    pub fn body_size(&self) -> u64 {
111        self.body.len() as u64
112    }
113
114    /// Is this a success (2xx)?
115    pub fn is_success(&self) -> bool {
116        self.status.is_success()
117    }
118
119    /// Is this a client error (4xx)?
120    pub fn is_client_error(&self) -> bool {
121        self.status.is_client_error()
122    }
123
124    /// Is this a server error (5xx)?
125    pub fn is_server_error(&self) -> bool {
126        self.status.is_server_error()
127    }
128
129    /// Is this a rate limit (429)?
130    pub fn is_rate_limited(&self) -> bool {
131        self.status == StatusCode::TOO_MANY_REQUESTS
132    }
133
134    /// Is this forbidden (403)?
135    pub fn is_forbidden(&self) -> bool {
136        self.status == StatusCode::FORBIDDEN
137    }
138
139    /// Body as UTF-8 string
140    pub fn text(&self) -> Result<String, std::string::FromUtf8Error> {
141        String::from_utf8(self.body.to_vec())
142    }
143}