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>(
127        &self,
128    ) -> Result<Option<T>, serde_json::Error> {
129        match &self.post_data {
130            Some(data) => serde_json::from_str(data).map(Some),
131            None => Ok(None),
132        }
133    }
134
135    /// Get the resource type.
136    pub fn resource_type(&self) -> ResourceType {
137        self.resource_type
138    }
139
140    /// Get the frame ID.
141    pub fn frame_id(&self) -> &str {
142        &self.frame_id
143    }
144
145    /// Check if this is a navigation request.
146    pub fn is_navigation_request(&self) -> bool {
147        self.is_navigation
148    }
149
150    /// Get the request that caused this redirect, if any.
151    pub fn redirected_from(&self) -> Option<&Request> {
152        self.redirected_from.as_deref()
153    }
154
155    /// Get the request that this request redirected to, if any.
156    pub fn redirected_to(&self) -> Option<&Request> {
157        self.redirected_to.as_deref()
158    }
159
160    /// Get request timing information.
161    pub fn timing(&self) -> Option<&RequestTiming> {
162        self.timing.as_ref()
163    }
164
165    /// Get request size information.
166    ///
167    /// Returns request body size and headers size.
168    pub async fn sizes(&self) -> RequestSizes {
169        let body_size = self.post_data.as_ref().map_or(0, std::string::String::len);
170        let headers_size = self
171            .headers
172            .iter()
173            .map(|(k, v)| k.len() + v.len() + 4) // ": " and "\r\n"
174            .sum();
175
176        RequestSizes {
177            request_body_size: body_size,
178            request_headers_size: headers_size,
179        }
180    }
181
182    /// Get the failure reason if the request failed.
183    pub fn failure(&self) -> Option<&str> {
184        self.failure_text.as_deref()
185    }
186
187    /// Set the navigation flag.
188    pub(crate) fn set_is_navigation(&mut self, is_navigation: bool) {
189        self.is_navigation = is_navigation;
190    }
191
192    /// Set the redirect chain (previous request that redirected to this one).
193    pub(crate) fn set_redirected_from(&mut self, from: Request) {
194        self.redirected_from = Some(Box::new(from));
195    }
196
197    /// Set the redirect target (next request in the redirect chain).
198    pub(crate) fn set_redirected_to(&mut self, to: Request) {
199        self.redirected_to = Some(Box::new(to));
200    }
201
202    /// Set timing information.
203    pub(crate) fn set_timing(&mut self, timing: RequestTiming) {
204        self.timing = Some(timing);
205    }
206
207    /// Set the failure text.
208    pub(crate) fn set_failure_text(&mut self, text: String) {
209        self.failure_text = Some(text);
210    }
211}
212
213/// Request timing information.
214#[derive(Debug, Clone)]
215pub struct RequestTiming {
216    /// Request start time in milliseconds.
217    pub start_time: f64,
218    /// Time spent resolving proxy.
219    pub proxy_start: f64,
220    pub proxy_end: f64,
221    /// Time spent resolving DNS.
222    pub dns_start: f64,
223    pub dns_end: f64,
224    /// Time spent connecting.
225    pub connect_start: f64,
226    pub connect_end: f64,
227    /// Time spent in SSL handshake.
228    pub ssl_start: f64,
229    pub ssl_end: f64,
230    /// Time sending the request.
231    pub send_start: f64,
232    pub send_end: f64,
233    /// Time receiving response headers.
234    pub receive_headers_start: f64,
235    pub receive_headers_end: f64,
236}
237
238impl From<viewpoint_cdp::protocol::network::ResourceTiming> for RequestTiming {
239    fn from(timing: viewpoint_cdp::protocol::network::ResourceTiming) -> Self {
240        Self {
241            start_time: timing.request_time * 1000.0,
242            proxy_start: timing.proxy_start,
243            proxy_end: timing.proxy_end,
244            dns_start: timing.dns_start,
245            dns_end: timing.dns_end,
246            connect_start: timing.connect_start,
247            connect_end: timing.connect_end,
248            ssl_start: timing.ssl_start,
249            ssl_end: timing.ssl_end,
250            send_start: timing.send_start,
251            send_end: timing.send_end,
252            receive_headers_start: timing.receive_headers_start.unwrap_or(0.0),
253            receive_headers_end: timing.receive_headers_end.unwrap_or(0.0),
254        }
255    }
256}
257
258/// Request size information.
259#[derive(Debug, Clone, Copy)]
260pub struct RequestSizes {
261    /// Size of the request body in bytes.
262    pub request_body_size: usize,
263    /// Size of the request headers in bytes.
264    pub request_headers_size: usize,
265}
266
267#[cfg(test)]
268mod tests;