viewpoint_core/network/request/
mod.rs

1//! Network request types.
2
3// Allow dead code for request builder setters (spec: network-events)
4
5use std::collections::HashMap;
6use std::sync::Arc;
7
8use viewpoint_cdp::CdpConnection;
9
10use super::types::ResourceType;
11
12/// A network request.
13///
14/// This type represents an HTTP request and provides access to request details
15/// such as URL, method, headers, and body.
16#[derive(Debug, Clone)]
17pub struct Request {
18    /// Request URL.
19    pub(crate) url: String,
20    /// HTTP method.
21    pub(crate) method: String,
22    /// Request headers.
23    pub(crate) headers: HashMap<String, String>,
24    /// POST data (if any).
25    pub(crate) post_data: Option<String>,
26    /// Resource type.
27    pub(crate) resource_type: ResourceType,
28    /// Frame ID.
29    pub(crate) frame_id: String,
30    /// Whether this is a navigation request.
31    pub(crate) is_navigation: bool,
32    /// CDP connection for fetching additional data.
33    pub(crate) connection: Option<Arc<CdpConnection>>,
34    /// Session ID for CDP commands.
35    pub(crate) session_id: Option<String>,
36    /// Network request ID.
37    pub(crate) request_id: Option<String>,
38    /// The request that this request was redirected from (previous in chain).
39    pub(crate) redirected_from: Option<Box<Request>>,
40    /// The request that this request redirected to (next in chain).
41    /// Note: This is wrapped in Arc to allow updating after creation.
42    pub(crate) redirected_to: Option<Box<Request>>,
43    /// Request timing information.
44    pub(crate) timing: Option<RequestTiming>,
45    /// Failure text if the request failed.
46    pub(crate) failure_text: Option<String>,
47}
48
49impl Request {
50    /// Create a new request from CDP request data.
51    pub(crate) fn from_cdp(
52        cdp_request: viewpoint_cdp::protocol::network::Request,
53        resource_type: viewpoint_cdp::protocol::network::ResourceType,
54        frame_id: String,
55        connection: Option<Arc<CdpConnection>>,
56        session_id: Option<String>,
57        request_id: Option<String>,
58    ) -> Self {
59        Self {
60            url: cdp_request.url,
61            method: cdp_request.method,
62            headers: cdp_request.headers,
63            post_data: cdp_request.post_data,
64            resource_type: resource_type.into(),
65            frame_id,
66            is_navigation: false, // Will be set separately
67            connection,
68            session_id,
69            request_id,
70            redirected_from: None,
71            redirected_to: None,
72            timing: None,
73            failure_text: None,
74        }
75    }
76
77    /// Get the request URL.
78    pub fn url(&self) -> &str {
79        &self.url
80    }
81
82    /// Get the HTTP method.
83    pub fn method(&self) -> &str {
84        &self.method
85    }
86
87    /// Get the request headers.
88    ///
89    /// Note: Headers are case-insensitive but the map preserves the original case.
90    pub fn headers(&self) -> &HashMap<String, String> {
91        &self.headers
92    }
93
94    /// Get a header value by name (case-insensitive).
95    pub fn header_value(&self, name: &str) -> Option<&str> {
96        self.headers
97            .iter()
98            .find(|(k, _)| k.eq_ignore_ascii_case(name))
99            .map(|(_, v)| v.as_str())
100    }
101
102    /// Get all headers asynchronously.
103    ///
104    /// This may fetch additional headers that weren't available synchronously.
105    pub async fn all_headers(&self) -> HashMap<String, String> {
106        // For now, just return the cached headers
107        // In the future, we could fetch security headers via CDP
108        self.headers.clone()
109    }
110
111    /// Get the POST data.
112    pub fn post_data(&self) -> Option<&str> {
113        self.post_data.as_deref()
114    }
115
116    /// Get the POST data as bytes.
117    pub fn post_data_buffer(&self) -> Option<Vec<u8>> {
118        self.post_data.as_ref().map(|s| s.as_bytes().to_vec())
119    }
120
121    /// Parse the POST data as JSON.
122    ///
123    /// # Errors
124    ///
125    /// Returns an error if the data is not valid JSON or doesn't match type T.
126    pub fn post_data_json<T: serde::de::DeserializeOwned>(&self) -> Result<Option<T>, serde_json::Error> {
127        match &self.post_data {
128            Some(data) => serde_json::from_str(data).map(Some),
129            None => Ok(None),
130        }
131    }
132
133    /// Get the resource type.
134    pub fn resource_type(&self) -> ResourceType {
135        self.resource_type
136    }
137
138    /// Get the frame ID.
139    pub fn frame_id(&self) -> &str {
140        &self.frame_id
141    }
142
143    /// Check if this is a navigation request.
144    pub fn is_navigation_request(&self) -> bool {
145        self.is_navigation
146    }
147
148    /// Get the request that caused this redirect, if any.
149    pub fn redirected_from(&self) -> Option<&Request> {
150        self.redirected_from.as_deref()
151    }
152
153    /// Get the request that this request redirected to, if any.
154    pub fn redirected_to(&self) -> Option<&Request> {
155        self.redirected_to.as_deref()
156    }
157
158    /// Get request timing information.
159    pub fn timing(&self) -> Option<&RequestTiming> {
160        self.timing.as_ref()
161    }
162
163    /// Get request size information.
164    ///
165    /// Returns request body size and headers size.
166    pub async fn sizes(&self) -> RequestSizes {
167        let body_size = self.post_data.as_ref().map_or(0, std::string::String::len);
168        let headers_size = self
169            .headers
170            .iter()
171            .map(|(k, v)| k.len() + v.len() + 4) // ": " and "\r\n"
172            .sum();
173
174        RequestSizes {
175            request_body_size: body_size,
176            request_headers_size: headers_size,
177        }
178    }
179
180    /// Get the failure reason if the request failed.
181    pub fn failure(&self) -> Option<&str> {
182        self.failure_text.as_deref()
183    }
184
185    /// Set the navigation flag.
186    pub(crate) fn set_is_navigation(&mut self, is_navigation: bool) {
187        self.is_navigation = is_navigation;
188    }
189
190    /// Set the redirect chain (previous request that redirected to this one).
191    pub(crate) fn set_redirected_from(&mut self, from: Request) {
192        self.redirected_from = Some(Box::new(from));
193    }
194
195    /// Set the redirect target (next request in the redirect chain).
196    pub(crate) fn set_redirected_to(&mut self, to: Request) {
197        self.redirected_to = Some(Box::new(to));
198    }
199
200    /// Set timing information.
201    pub(crate) fn set_timing(&mut self, timing: RequestTiming) {
202        self.timing = Some(timing);
203    }
204
205    /// Set the failure text.
206    pub(crate) fn set_failure_text(&mut self, text: String) {
207        self.failure_text = Some(text);
208    }
209}
210
211/// Request timing information.
212#[derive(Debug, Clone)]
213pub struct RequestTiming {
214    /// Request start time in milliseconds.
215    pub start_time: f64,
216    /// Time spent resolving proxy.
217    pub proxy_start: f64,
218    pub proxy_end: f64,
219    /// Time spent resolving DNS.
220    pub dns_start: f64,
221    pub dns_end: f64,
222    /// Time spent connecting.
223    pub connect_start: f64,
224    pub connect_end: f64,
225    /// Time spent in SSL handshake.
226    pub ssl_start: f64,
227    pub ssl_end: f64,
228    /// Time sending the request.
229    pub send_start: f64,
230    pub send_end: f64,
231    /// Time receiving response headers.
232    pub receive_headers_start: f64,
233    pub receive_headers_end: f64,
234}
235
236impl From<viewpoint_cdp::protocol::network::ResourceTiming> for RequestTiming {
237    fn from(timing: viewpoint_cdp::protocol::network::ResourceTiming) -> Self {
238        Self {
239            start_time: timing.request_time * 1000.0,
240            proxy_start: timing.proxy_start,
241            proxy_end: timing.proxy_end,
242            dns_start: timing.dns_start,
243            dns_end: timing.dns_end,
244            connect_start: timing.connect_start,
245            connect_end: timing.connect_end,
246            ssl_start: timing.ssl_start,
247            ssl_end: timing.ssl_end,
248            send_start: timing.send_start,
249            send_end: timing.send_end,
250            receive_headers_start: timing.receive_headers_start.unwrap_or(0.0),
251            receive_headers_end: timing.receive_headers_end.unwrap_or(0.0),
252        }
253    }
254}
255
256/// Request size information.
257#[derive(Debug, Clone, Copy)]
258pub struct RequestSizes {
259    /// Size of the request body in bytes.
260    pub request_body_size: usize,
261    /// Size of the request headers in bytes.
262    pub request_headers_size: usize,
263}
264
265#[cfg(test)]
266mod tests;