viewpoint_core/network/response/
mod.rs

1//! Network response types.
2
3use std::collections::HashMap;
4use std::sync::Arc;
5
6use viewpoint_cdp::protocol::network::GetResponseBodyParams;
7use viewpoint_cdp::CdpConnection;
8
9use super::request::Request;
10use crate::error::NetworkError;
11
12/// A network response.
13///
14/// This type represents an HTTP response and provides access to response details
15/// such as status, headers, and body.
16#[derive(Debug, Clone)]
17pub struct Response {
18    /// Response URL.
19    url: String,
20    /// HTTP status code.
21    status: u16,
22    /// HTTP status text.
23    status_text: String,
24    /// Response headers.
25    headers: HashMap<String, String>,
26    /// MIME type.
27    mime_type: String,
28    /// Whether response was served from cache.
29    from_cache: bool,
30    /// Whether response was served from service worker.
31    from_service_worker: bool,
32    /// Associated request.
33    request: Request,
34    /// CDP connection for fetching body.
35    connection: Arc<CdpConnection>,
36    /// Session ID for CDP commands.
37    session_id: String,
38    /// Network request ID for fetching body.
39    request_id: String,
40    /// Security details (for HTTPS).
41    security_details: Option<SecurityDetails>,
42    /// Remote server address.
43    remote_address: Option<RemoteAddress>,
44}
45
46impl Response {
47    /// Create a new response from CDP response data.
48    #[allow(clippy::too_many_arguments)]
49    pub(crate) fn new(
50        cdp_response: viewpoint_cdp::protocol::network::Response,
51        request: Request,
52        connection: Arc<CdpConnection>,
53        session_id: String,
54        request_id: String,
55    ) -> Self {
56        let remote_address = cdp_response.remote_ip_address.map(|ip| RemoteAddress {
57            ip_address: ip,
58            port: cdp_response.remote_port.unwrap_or(0) as u16,
59        });
60
61        // Convert security details from CDP type
62        let security_details = cdp_response.security_details.map(SecurityDetails::from);
63
64        Self {
65            url: cdp_response.url,
66            status: cdp_response.status as u16,
67            status_text: cdp_response.status_text,
68            headers: cdp_response.headers,
69            mime_type: cdp_response.mime_type,
70            from_cache: cdp_response.from_disk_cache.unwrap_or(false),
71            from_service_worker: cdp_response.from_service_worker.unwrap_or(false),
72            request,
73            connection,
74            session_id,
75            request_id,
76            security_details,
77            remote_address,
78        }
79    }
80
81    /// Get the response URL.
82    ///
83    /// This may differ from the request URL in case of redirects.
84    pub fn url(&self) -> &str {
85        &self.url
86    }
87
88    /// Get the HTTP status code.
89    pub fn status(&self) -> u16 {
90        self.status
91    }
92
93    /// Get the HTTP status text.
94    pub fn status_text(&self) -> &str {
95        &self.status_text
96    }
97
98    /// Check if the response was successful (status 200-299).
99    pub fn ok(&self) -> bool {
100        (200..300).contains(&self.status)
101    }
102
103    /// Get the response headers.
104    pub fn headers(&self) -> &HashMap<String, String> {
105        &self.headers
106    }
107
108    /// Get a header value by name (case-insensitive).
109    pub fn header_value(&self, name: &str) -> Option<&str> {
110        self.headers
111            .iter()
112            .find(|(k, _)| k.eq_ignore_ascii_case(name))
113            .map(|(_, v)| v.as_str())
114    }
115
116    /// Get all headers asynchronously.
117    ///
118    /// This may fetch additional headers that weren't available synchronously.
119    pub async fn all_headers(&self) -> HashMap<String, String> {
120        // For now, just return the cached headers
121        self.headers.clone()
122    }
123
124    /// Get the MIME type.
125    pub fn mime_type(&self) -> &str {
126        &self.mime_type
127    }
128
129    /// Check if the response was served from cache.
130    pub fn from_cache(&self) -> bool {
131        self.from_cache
132    }
133
134    /// Check if the response was served from a service worker.
135    pub fn from_service_worker(&self) -> bool {
136        self.from_service_worker
137    }
138
139    /// Get the associated request.
140    pub fn request(&self) -> &Request {
141        &self.request
142    }
143
144    /// Get the response body as bytes.
145    ///
146    /// # Errors
147    ///
148    /// Returns an error if the body cannot be fetched.
149    pub async fn body(&self) -> Result<Vec<u8>, NetworkError> {
150        let result: viewpoint_cdp::protocol::network::GetResponseBodyResult = self
151            .connection
152            .send_command(
153                "Network.getResponseBody",
154                Some(GetResponseBodyParams {
155                    request_id: self.request_id.clone(),
156                }),
157                Some(&self.session_id),
158            )
159            .await
160            .map_err(NetworkError::from)?;
161
162        if result.base64_encoded {
163            use base64::Engine;
164            base64::engine::general_purpose::STANDARD
165                .decode(&result.body)
166                .map_err(|e| NetworkError::InvalidResponse(format!("Failed to decode base64: {e}")))
167        } else {
168            Ok(result.body.into_bytes())
169        }
170    }
171
172    /// Get the response body as text.
173    ///
174    /// # Errors
175    ///
176    /// Returns an error if the body cannot be fetched or is not valid UTF-8.
177    pub async fn text(&self) -> Result<String, NetworkError> {
178        let body = self.body().await?;
179        String::from_utf8(body)
180            .map_err(|e| NetworkError::InvalidResponse(format!("Response is not valid UTF-8: {e}")))
181    }
182
183    /// Parse the response body as JSON.
184    ///
185    /// # Errors
186    ///
187    /// Returns an error if the body cannot be fetched or is not valid JSON.
188    pub async fn json<T: serde::de::DeserializeOwned>(&self) -> Result<T, NetworkError> {
189        let text = self.text().await?;
190        serde_json::from_str(&text)
191            .map_err(|e| NetworkError::InvalidResponse(format!("Failed to parse JSON: {e}")))
192    }
193
194    /// Get security details for HTTPS responses.
195    pub fn security_details(&self) -> Option<&SecurityDetails> {
196        self.security_details.as_ref()
197    }
198
199    /// Get the remote server address.
200    pub fn server_addr(&self) -> Option<&RemoteAddress> {
201        self.remote_address.as_ref()
202    }
203
204    /// Wait for the response body to be fully received.
205    pub async fn finished(&self) -> Result<(), NetworkError> {
206        // For responses that are already complete, this is a no-op
207        // For streaming responses, we would need to wait for loadingFinished event
208        Ok(())
209    }
210}
211
212/// Security details for HTTPS responses.
213#[derive(Debug, Clone)]
214pub struct SecurityDetails {
215    /// TLS protocol name (e.g., "TLS 1.3").
216    pub protocol: String,
217    /// Certificate subject name.
218    pub subject_name: String,
219    /// Certificate issuer.
220    pub issuer: String,
221    /// Certificate valid from (Unix timestamp).
222    pub valid_from: f64,
223    /// Certificate valid to (Unix timestamp).
224    pub valid_to: f64,
225    /// Subject Alternative Names.
226    pub san_list: Vec<String>,
227}
228
229impl From<viewpoint_cdp::protocol::network::SecurityDetails> for SecurityDetails {
230    fn from(details: viewpoint_cdp::protocol::network::SecurityDetails) -> Self {
231        Self {
232            protocol: details.protocol,
233            subject_name: details.subject_name,
234            issuer: details.issuer,
235            valid_from: details.valid_from,
236            valid_to: details.valid_to,
237            san_list: details.san_list,
238        }
239    }
240}
241
242/// Remote server address.
243#[derive(Debug, Clone)]
244pub struct RemoteAddress {
245    /// IP address.
246    pub ip_address: String,
247    /// Port number.
248    pub port: u16,
249}
250
251#[cfg(test)]
252mod tests;