Skip to main content

playwright_rs/protocol/
response.rs

1// Response protocol object
2//
3// Represents an HTTP response from navigation operations.
4// Response objects are created by the server when Frame.goto() or similar navigation
5// methods complete successfully.
6
7use crate::error::Result;
8use crate::server::channel_owner::{ChannelOwner, ChannelOwnerImpl, ParentOrConnection};
9use serde_json::Value;
10use std::any::Any;
11use std::sync::Arc;
12
13/// TLS/SSL security details for an HTTPS response.
14///
15/// All fields are optional — the server provides what's available.
16///
17/// See: <https://playwright.dev/docs/api/class-response#response-security-details>
18#[derive(Debug, Clone, serde::Deserialize)]
19#[serde(rename_all = "camelCase")]
20#[non_exhaustive]
21pub struct SecurityDetails {
22    /// Certificate issuer name.
23    pub issuer: Option<String>,
24    /// TLS protocol version (e.g., "TLS 1.3").
25    pub protocol: Option<String>,
26    /// Certificate subject name.
27    pub subject_name: Option<String>,
28    /// Unix timestamp (seconds) when the certificate becomes valid.
29    pub valid_from: Option<f64>,
30    /// Unix timestamp (seconds) when the certificate expires.
31    pub valid_to: Option<f64>,
32}
33
34/// Remote server address (IP and port).
35///
36/// See: <https://playwright.dev/docs/api/class-response#response-server-addr>
37#[derive(Debug, Clone, serde::Deserialize)]
38#[serde(rename_all = "camelCase")]
39#[non_exhaustive]
40pub struct RemoteAddr {
41    /// Server IP address.
42    pub ip_address: String,
43    /// Server port.
44    pub port: u16,
45}
46
47/// Resource size information for a request/response pair.
48///
49/// See: <https://playwright.dev/docs/api/class-request#request-sizes>
50#[derive(Debug, Clone)]
51#[non_exhaustive]
52pub struct RequestSizes {
53    /// Size of the request body in bytes. Set to 0 if there was no body.
54    pub request_body_size: i64,
55    /// Total number of bytes from the start of the HTTP request message
56    /// until (and including) the double CRLF before the body.
57    pub request_headers_size: i64,
58    /// Size of the received response body in bytes.
59    pub response_body_size: i64,
60    /// Total number of bytes from the start of the HTTP response message
61    /// until (and including) the double CRLF before the body.
62    pub response_headers_size: i64,
63}
64
65/// A single HTTP header entry with a name and value.
66///
67/// Used by `Response::headers_array()` to return all headers preserving duplicates.
68///
69/// See: <https://playwright.dev/docs/api/class-response#response-headers-array>
70#[derive(Debug, Clone)]
71#[non_exhaustive]
72pub struct HeaderEntry {
73    /// Header name (lowercase)
74    pub name: String,
75    /// Header value
76    pub value: String,
77}
78
79/// Response represents an HTTP response from a navigation operation.
80///
81/// Response objects are not created directly - they are returned from
82/// navigation methods like page.goto() or page.reload().
83///
84/// See: <https://playwright.dev/docs/api/class-response>
85#[derive(Clone)]
86pub struct ResponseObject {
87    base: ChannelOwnerImpl,
88}
89
90impl ResponseObject {
91    /// Creates a new Response from protocol initialization
92    ///
93    /// This is called by the object factory when the server sends a `__create__` message
94    /// for a Response object.
95    pub fn new(
96        parent: Arc<dyn ChannelOwner>,
97        type_name: String,
98        guid: Arc<str>,
99        initializer: Value,
100    ) -> Result<Self> {
101        let base = ChannelOwnerImpl::new(
102            ParentOrConnection::Parent(parent),
103            type_name,
104            guid,
105            initializer,
106        );
107
108        Ok(Self { base })
109    }
110
111    /// Returns the status code of the response (e.g., 200 for a success).
112    ///
113    /// See: <https://playwright.dev/docs/api/class-response#response-status>
114    pub fn status(&self) -> u16 {
115        self.initializer()
116            .get("status")
117            .and_then(|v| v.as_u64())
118            .unwrap_or(0) as u16
119    }
120
121    /// Returns the status text of the response (e.g. usually an "OK" for a success).
122    ///
123    /// See: <https://playwright.dev/docs/api/class-response#response-status-text>
124    pub fn status_text(&self) -> &str {
125        self.initializer()
126            .get("statusText")
127            .and_then(|v| v.as_str())
128            .unwrap_or("")
129    }
130
131    /// Returns the URL of the response.
132    ///
133    /// See: <https://playwright.dev/docs/api/class-response#response-url>
134    pub fn url(&self) -> &str {
135        self.initializer()
136            .get("url")
137            .and_then(|v| v.as_str())
138            .unwrap_or("")
139    }
140
141    /// Returns the response body as bytes.
142    ///
143    /// Sends a `"body"` RPC call to the Playwright server, which returns the body
144    /// as a base64-encoded binary string.
145    ///
146    /// See: <https://playwright.dev/docs/api/class-response#response-body>
147    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid(), bytes_len = tracing::field::Empty))]
148    pub async fn body(&self) -> Result<Vec<u8>> {
149        use serde::Deserialize;
150
151        #[derive(Deserialize)]
152        struct BodyResponse {
153            binary: String,
154        }
155
156        let result: BodyResponse = self.channel().send("body", serde_json::json!({})).await?;
157
158        use base64::Engine;
159        let bytes = base64::engine::general_purpose::STANDARD
160            .decode(&result.binary)
161            .map_err(|e| {
162                crate::error::Error::ProtocolError(format!(
163                    "Failed to decode response body from base64: {}",
164                    e
165                ))
166            })?;
167        Ok(bytes)
168    }
169
170    /// Returns TLS/SSL security details for HTTPS connections, or `None` for HTTP.
171    ///
172    /// See: <https://playwright.dev/docs/api/class-response#response-security-details>
173    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
174    pub async fn security_details(&self) -> Result<Option<SecurityDetails>> {
175        let result: serde_json::Value = self
176            .channel()
177            .send("securityDetails", serde_json::json!({}))
178            .await?;
179
180        let value = result.get("value");
181        match value {
182            Some(v) if v.as_object().is_some_and(|obj| !obj.is_empty()) => {
183                Ok(Some(SecurityDetails {
184                    issuer: v.get("issuer").and_then(|v| v.as_str()).map(String::from),
185                    protocol: v.get("protocol").and_then(|v| v.as_str()).map(String::from),
186                    subject_name: v
187                        .get("subjectName")
188                        .and_then(|v| v.as_str())
189                        .map(String::from),
190                    valid_from: v.get("validFrom").and_then(|v| v.as_f64()),
191                    valid_to: v.get("validTo").and_then(|v| v.as_f64()),
192                }))
193            }
194            _ => Ok(None),
195        }
196    }
197
198    /// Returns the server's IP address and port for this response, or `None`.
199    ///
200    /// See: <https://playwright.dev/docs/api/class-response#response-server-addr>
201    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
202    pub async fn server_addr(&self) -> Result<Option<RemoteAddr>> {
203        let result: serde_json::Value = self
204            .channel()
205            .send("serverAddr", serde_json::json!({}))
206            .await?;
207
208        let value = result.get("value");
209        match value {
210            Some(v) if !v.is_null() => {
211                let ip_address = v
212                    .get("ipAddress")
213                    .and_then(|v| v.as_str())
214                    .unwrap_or("")
215                    .to_string();
216                let port = v.get("port").and_then(|v| v.as_u64()).unwrap_or(0) as u16;
217                Ok(Some(RemoteAddr { ip_address, port }))
218            }
219            _ => Ok(None),
220        }
221    }
222
223    /// Returns resource size information for this response.
224    ///
225    /// See: <https://playwright.dev/docs/api/class-request#request-sizes>
226    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
227    pub async fn sizes(&self) -> Result<RequestSizes> {
228        use serde::Deserialize;
229
230        #[derive(Deserialize)]
231        #[serde(rename_all = "camelCase")]
232        struct SizesRaw {
233            request_body_size: i64,
234            request_headers_size: i64,
235            response_body_size: i64,
236            response_headers_size: i64,
237        }
238
239        #[derive(Deserialize)]
240        struct RpcResult {
241            sizes: SizesRaw,
242        }
243
244        let result: RpcResult = self.channel().send("sizes", serde_json::json!({})).await?;
245
246        Ok(RequestSizes {
247            request_body_size: result.sizes.request_body_size,
248            request_headers_size: result.sizes.request_headers_size,
249            response_body_size: result.sizes.response_body_size,
250            response_headers_size: result.sizes.response_headers_size,
251        })
252    }
253
254    /// Returns the HTTP version used by this response (e.g. `"HTTP/1.1"` or `"HTTP/2.0"`).
255    ///
256    /// Sends a `"httpVersion"` RPC call to the Playwright server.
257    ///
258    /// See: <https://playwright.dev/docs/api/class-response#response-http-version>
259    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid(), version = tracing::field::Empty))]
260    pub async fn http_version(&self) -> Result<String> {
261        use serde::Deserialize;
262
263        #[derive(Deserialize)]
264        struct HttpVersionResponse {
265            value: String,
266        }
267
268        let result: HttpVersionResponse = self
269            .channel()
270            .send("httpVersion", serde_json::json!({}))
271            .await?;
272        Ok(result.value)
273    }
274
275    /// Returns the raw response headers as name-value pairs (preserving duplicates).
276    ///
277    /// Sends a `"rawResponseHeaders"` RPC call to the Playwright server.
278    ///
279    /// See: <https://playwright.dev/docs/api/class-response#response-headers-array>
280    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
281    pub async fn raw_headers(&self) -> Result<Vec<HeaderEntry>> {
282        use serde::Deserialize;
283
284        #[derive(Deserialize)]
285        struct RawHeadersResponse {
286            headers: Vec<HeaderEntryRaw>,
287        }
288
289        #[derive(Deserialize)]
290        struct HeaderEntryRaw {
291            name: String,
292            value: String,
293        }
294
295        let result: RawHeadersResponse = self
296            .channel()
297            .send("rawResponseHeaders", serde_json::json!({}))
298            .await?;
299
300        Ok(result
301            .headers
302            .into_iter()
303            .map(|h| HeaderEntry {
304                name: h.name,
305                value: h.value,
306            })
307            .collect())
308    }
309}
310
311impl ChannelOwner for ResponseObject {
312    fn guid(&self) -> &str {
313        self.base.guid()
314    }
315
316    fn type_name(&self) -> &str {
317        self.base.type_name()
318    }
319
320    fn parent(&self) -> Option<Arc<dyn ChannelOwner>> {
321        self.base.parent()
322    }
323
324    fn connection(&self) -> Arc<dyn crate::server::connection::ConnectionLike> {
325        self.base.connection()
326    }
327
328    fn initializer(&self) -> &Value {
329        self.base.initializer()
330    }
331
332    fn channel(&self) -> &crate::server::channel::Channel {
333        self.base.channel()
334    }
335
336    fn dispose(&self, reason: crate::server::channel_owner::DisposeReason) {
337        self.base.dispose(reason)
338    }
339
340    fn adopt(&self, child: Arc<dyn ChannelOwner>) {
341        self.base.adopt(child)
342    }
343
344    fn add_child(&self, guid: Arc<str>, child: Arc<dyn ChannelOwner>) {
345        self.base.add_child(guid, child)
346    }
347
348    fn remove_child(&self, guid: &str) {
349        self.base.remove_child(guid)
350    }
351
352    fn on_event(&self, _method: &str, _params: Value) {
353        // Response objects don't have events
354    }
355
356    fn was_collected(&self) -> bool {
357        self.base.was_collected()
358    }
359
360    fn as_any(&self) -> &dyn Any {
361        self
362    }
363}
364
365impl std::fmt::Debug for ResponseObject {
366    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
367        f.debug_struct("ResponseObject")
368            .field("guid", &self.guid())
369            .finish()
370    }
371}
372
373#[cfg(test)]
374mod security_remote_addr_tests {
375    use super::{RemoteAddr, SecurityDetails};
376
377    #[test]
378    fn security_details_deserializes_camelcase() {
379        let v = serde_json::json!({
380            "issuer": "Let's Encrypt",
381            "protocol": "TLS 1.3",
382            "subjectName": "example.com",
383            "validFrom": 1_700_000_000.0,
384            "validTo": 1_800_000_000.0
385        });
386        let sd: SecurityDetails = serde_json::from_value(v).unwrap();
387        assert_eq!(sd.issuer.as_deref(), Some("Let's Encrypt"));
388        assert_eq!(sd.subject_name.as_deref(), Some("example.com"));
389        assert_eq!(sd.protocol.as_deref(), Some("TLS 1.3"));
390        assert_eq!(sd.valid_from, Some(1_700_000_000.0));
391        assert_eq!(sd.valid_to, Some(1_800_000_000.0));
392    }
393
394    #[test]
395    fn security_details_allows_missing_fields() {
396        let sd: SecurityDetails = serde_json::from_value(serde_json::json!({})).unwrap();
397        assert!(sd.issuer.is_none() && sd.protocol.is_none() && sd.subject_name.is_none());
398    }
399
400    #[test]
401    fn remote_addr_deserializes_camelcase() {
402        let v = serde_json::json!({ "ipAddress": "127.0.0.1", "port": 8080 });
403        let addr: RemoteAddr = serde_json::from_value(v).unwrap();
404        assert_eq!(addr.ip_address, "127.0.0.1");
405        assert_eq!(addr.port, 8080);
406    }
407}