Skip to main content

playwright_rs/protocol/
request.rs

1// Request protocol object
2//
3// Represents an HTTP request. Created during navigation operations.
4// In Playwright's architecture, navigation creates a Request which receives a Response.
5
6use crate::error::Result;
7use crate::protocol::response::HeaderEntry;
8use crate::server::channel_owner::{ChannelOwner, ChannelOwnerImpl, ParentOrConnection};
9use crate::server::connection::ConnectionExt;
10use serde::de::DeserializeOwned;
11use serde_json::Value;
12use std::any::Any;
13use std::collections::HashMap;
14use std::sync::{Arc, Mutex};
15
16/// Request represents an HTTP request during navigation.
17///
18/// Request objects are created by the server during navigation operations.
19/// They are parents to Response objects.
20///
21/// See: <https://playwright.dev/docs/api/class-request>
22#[derive(Clone)]
23pub struct Request {
24    base: ChannelOwnerImpl,
25    /// Failure text set when a `requestFailed` event is received for this request.
26    failure_text: Arc<Mutex<Option<String>>>,
27    /// Timing data set when the associated `requestFinished` event fires.
28    /// The value is the raw JSON timing object from the Response initializer.
29    timing: Arc<Mutex<Option<Value>>>,
30    /// Eagerly resolved Frame back-reference from the initializer's `frame.guid`.
31    frame: Arc<Mutex<Option<crate::protocol::Frame>>>,
32    /// The request that redirected to this one (from initializer `redirectedFrom`).
33    redirected_from: Arc<Mutex<Option<Request>>>,
34    /// The request that this one redirected to (set by the later request's construction).
35    redirected_to: Arc<Mutex<Option<Request>>>,
36}
37
38impl Request {
39    /// Creates a new Request from protocol initialization
40    ///
41    /// This is called by the object factory when the server sends a `__create__` message
42    /// for a Request object.
43    pub fn new(
44        parent: Arc<dyn ChannelOwner>,
45        type_name: String,
46        guid: Arc<str>,
47        initializer: Value,
48    ) -> Result<Self> {
49        let base = ChannelOwnerImpl::new(
50            ParentOrConnection::Parent(parent),
51            type_name,
52            guid,
53            initializer,
54        );
55
56        Ok(Self {
57            base,
58            failure_text: Arc::new(Mutex::new(None)),
59            timing: Arc::new(Mutex::new(None)),
60            frame: Arc::new(Mutex::new(None)),
61            redirected_from: Arc::new(Mutex::new(None)),
62            redirected_to: Arc::new(Mutex::new(None)),
63        })
64    }
65
66    /// Returns the [`Frame`](crate::protocol::Frame) that initiated this request.
67    ///
68    /// The frame is resolved from the `frame` GUID in the protocol initializer data.
69    ///
70    /// See: <https://playwright.dev/docs/api/class-request#request-frame>
71    pub fn frame(&self) -> Option<crate::protocol::Frame> {
72        self.frame.lock().unwrap().clone()
73    }
74
75    /// Returns the request that redirected to this one, or `None`.
76    ///
77    /// When the server responds with a redirect, Playwright creates a new Request
78    /// for the redirect target. The new request's `redirected_from` points back to
79    /// the original request.
80    ///
81    /// See: <https://playwright.dev/docs/api/class-request#request-redirected-from>
82    pub fn redirected_from(&self) -> Option<Request> {
83        self.redirected_from.lock().unwrap().clone()
84    }
85
86    /// Returns the request that this one redirected to, or `None`.
87    ///
88    /// This is the inverse of `redirected_from()`: if request A redirected to
89    /// request B, then `A.redirected_to()` returns B.
90    ///
91    /// See: <https://playwright.dev/docs/api/class-request#request-redirected-to>
92    pub fn redirected_to(&self) -> Option<Request> {
93        self.redirected_to.lock().unwrap().clone()
94    }
95
96    /// Sets the redirect-from back-pointer. Called by the object factory
97    /// when a new Request has `redirectedFrom` in its initializer.
98    pub(crate) fn set_redirected_from(&self, from: Request) {
99        *self.redirected_from.lock().unwrap() = Some(from);
100    }
101
102    /// Sets the redirect-to forward pointer. Called as a side-effect when
103    /// the redirect target request is constructed.
104    pub(crate) fn set_redirected_to(&self, to: Request) {
105        *self.redirected_to.lock().unwrap() = Some(to);
106    }
107
108    /// Returns the [`Response`](crate::protocol::response::ResponseObject) for this request.
109    ///
110    /// Sends a `"response"` RPC call to the Playwright server.
111    /// Returns `None` if the request has not received a response (e.g., it failed).
112    ///
113    /// See: <https://playwright.dev/docs/api/class-request#request-response>
114    pub async fn response(&self) -> Result<Option<crate::protocol::page::Response>> {
115        use serde::Deserialize;
116
117        #[derive(Deserialize)]
118        struct GuidRef {
119            guid: String,
120        }
121
122        #[derive(Deserialize)]
123        struct ResponseResult {
124            response: Option<GuidRef>,
125        }
126
127        let result: ResponseResult = self
128            .channel()
129            .send("response", serde_json::json!({}))
130            .await?;
131
132        let guid = match result.response {
133            Some(r) => r.guid,
134            None => return Ok(None),
135        };
136
137        let connection = self.connection();
138        // get_typed validates the type; get_object provides the Arc<dyn ChannelOwner>
139        // needed by Response::new for back-reference support
140        let response_obj: crate::protocol::ResponseObject = connection
141            .get_typed::<crate::protocol::ResponseObject>(&guid)
142            .await
143            .map_err(|e| {
144                crate::error::Error::ProtocolError(format!(
145                    "Failed to get Response object {}: {}",
146                    guid, e
147                ))
148            })?;
149        let response_arc = connection.get_object(&guid).await.map_err(|e| {
150            crate::error::Error::ProtocolError(format!(
151                "Failed to get Response object {}: {}",
152                guid, e
153            ))
154        })?;
155
156        let initializer = response_obj.initializer();
157        let status = initializer
158            .get("status")
159            .and_then(|v| v.as_u64())
160            .unwrap_or(0) as u16;
161        let headers: std::collections::HashMap<String, String> = initializer
162            .get("headers")
163            .and_then(|v| v.as_array())
164            .map(|arr| {
165                arr.iter()
166                    .filter_map(|h| {
167                        let name = h.get("name")?.as_str()?;
168                        let value = h.get("value")?.as_str()?;
169                        Some((name.to_string(), value.to_string()))
170                    })
171                    .collect()
172            })
173            .unwrap_or_default();
174
175        Ok(Some(crate::protocol::page::Response::new(
176            initializer
177                .get("url")
178                .and_then(|v| v.as_str())
179                .unwrap_or("")
180                .to_string(),
181            status,
182            initializer
183                .get("statusText")
184                .and_then(|v| v.as_str())
185                .unwrap_or("")
186                .to_string(),
187            headers,
188            Some(response_arc),
189        )))
190    }
191
192    /// Returns resource size information for this request.
193    ///
194    /// Internally fetches the associated Response (via RPC) and calls `sizes()`
195    /// on the response's channel.
196    ///
197    /// See: <https://playwright.dev/docs/api/class-request#request-sizes>
198    pub async fn sizes(&self) -> Result<crate::protocol::response::RequestSizes> {
199        let response = self.response().await?;
200        let response = response.ok_or_else(|| {
201            crate::error::Error::ProtocolError(
202                "Unable to fetch sizes for failed request".to_string(),
203            )
204        })?;
205
206        let response_obj = response.response_object().map_err(|_| {
207            crate::error::Error::ProtocolError(
208                "Response has no backing protocol object for sizes()".to_string(),
209            )
210        })?;
211
212        response_obj.sizes().await
213    }
214
215    /// Sets the eagerly-resolved Frame back-reference.
216    ///
217    /// Called by the object factory after the Request is created and the Frame
218    /// has been looked up from the connection registry.
219    pub(crate) fn set_frame(&self, frame: crate::protocol::Frame) {
220        *self.frame.lock().unwrap() = Some(frame);
221    }
222
223    /// Returns the URL of the request.
224    ///
225    /// See: <https://playwright.dev/docs/api/class-request#request-url>
226    pub fn url(&self) -> &str {
227        self.initializer()
228            .get("url")
229            .and_then(|v| v.as_str())
230            .unwrap_or("")
231    }
232
233    /// Returns the HTTP method of the request (GET, POST, etc.).
234    ///
235    /// See: <https://playwright.dev/docs/api/class-request#request-method>
236    pub fn method(&self) -> &str {
237        self.initializer()
238            .get("method")
239            .and_then(|v| v.as_str())
240            .unwrap_or("GET")
241    }
242
243    /// Returns the resource type of the request (e.g., "document", "stylesheet", "image", "fetch", etc.).
244    ///
245    /// See: <https://playwright.dev/docs/api/class-request#request-resource-type>
246    pub fn resource_type(&self) -> &str {
247        self.initializer()
248            .get("resourceType")
249            .and_then(|v| v.as_str())
250            .unwrap_or("other")
251    }
252
253    /// Check if this request is for a navigation (main document).
254    ///
255    /// A navigation request is when the request is for the main frame's document.
256    /// This is used to distinguish between main document loads and subresource loads.
257    ///
258    /// See: <https://playwright.dev/docs/api/class-request#request-is-navigation-request>
259    pub fn is_navigation_request(&self) -> bool {
260        self.resource_type() == "document"
261    }
262
263    /// Returns the request headers as a HashMap.
264    ///
265    /// The headers are read from the protocol initializer data. The format in the
266    /// protocol is a list of `{name, value}` objects which are merged into a
267    /// `HashMap<String, String>`. If duplicate header names exist, the last
268    /// value wins.
269    ///
270    /// For the full set of raw headers (including duplicates), use
271    /// [`headers_array()`](Self::headers_array) or [`all_headers()`](Self::all_headers).
272    ///
273    /// See: <https://playwright.dev/docs/api/class-request#request-headers>
274    pub fn headers(&self) -> HashMap<String, String> {
275        let mut map = HashMap::new();
276        if let Some(headers) = self.initializer().get("headers").and_then(|v| v.as_array()) {
277            for entry in headers {
278                if let (Some(name), Some(value)) = (
279                    entry.get("name").and_then(|v| v.as_str()),
280                    entry.get("value").and_then(|v| v.as_str()),
281                ) {
282                    map.insert(name.to_lowercase(), value.to_string());
283                }
284            }
285        }
286        map
287    }
288
289    /// Returns the raw base64-encoded post data from the initializer, or `None`.
290    fn post_data_b64(&self) -> Option<&str> {
291        self.initializer().get("postData").and_then(|v| v.as_str())
292    }
293
294    /// Returns the request body (POST data) as bytes, or `None` if there is no body.
295    ///
296    /// The Playwright protocol sends `postData` as a base64-encoded string.
297    /// This method decodes it to raw bytes.
298    ///
299    /// This is a local read and does not require an RPC call.
300    ///
301    /// See: <https://playwright.dev/docs/api/class-request#request-post-data-buffer>
302    pub fn post_data_buffer(&self) -> Option<Vec<u8>> {
303        let b64 = self.post_data_b64()?;
304        use base64::Engine;
305        base64::engine::general_purpose::STANDARD.decode(b64).ok()
306    }
307
308    /// Returns the request body (POST data) as a UTF-8 string, or `None` if there is no body.
309    ///
310    /// The Playwright protocol sends `postData` as a base64-encoded string.
311    /// This method decodes the base64 and then converts the bytes to a UTF-8 string.
312    ///
313    /// This is a local read and does not require an RPC call.
314    ///
315    /// See: <https://playwright.dev/docs/api/class-request#request-post-data>
316    pub fn post_data(&self) -> Option<String> {
317        let bytes = self.post_data_buffer()?;
318        String::from_utf8(bytes).ok()
319    }
320
321    /// Parses the POST data as JSON and deserializes into the target type `T`.
322    ///
323    /// Returns `None` if the request has no POST data, or `Some(Err(...))` if the
324    /// JSON parsing fails.
325    ///
326    /// See: <https://playwright.dev/docs/api/class-request#request-post-data-json>
327    pub fn post_data_json<T: DeserializeOwned>(&self) -> Option<Result<T>> {
328        let data = self.post_data()?;
329        Some(serde_json::from_str(&data).map_err(|e| {
330            crate::error::Error::ProtocolError(format!(
331                "Failed to parse request post data as JSON: {}",
332                e
333            ))
334        }))
335    }
336
337    /// Returns the error text if the request failed, or `None` for successful requests.
338    ///
339    /// The failure text is set when the `requestFailed` browser event fires for this
340    /// request. Use `page.on_request_failed()` to capture failed requests and then
341    /// call this method to get the error reason.
342    ///
343    /// See: <https://playwright.dev/docs/api/class-request#request-failure>
344    pub fn failure(&self) -> Option<String> {
345        self.failure_text.lock().unwrap().clone()
346    }
347
348    /// Sets the failure text. Called by the dispatcher when a `requestFailed` event
349    /// arrives for this request.
350    pub(crate) fn set_failure_text(&self, text: String) {
351        *self.failure_text.lock().unwrap() = Some(text);
352    }
353
354    /// Sets the timing data. Called by the dispatcher when a `requestFinished` event
355    /// arrives and timing data is extracted from the associated Response's initializer.
356    pub(crate) fn set_timing(&self, timing_val: Value) {
357        *self.timing.lock().unwrap() = Some(timing_val);
358    }
359
360    /// Returns all request headers as name-value pairs, preserving duplicates.
361    ///
362    /// Sends a `"rawRequestHeaders"` RPC call to the Playwright server which returns
363    /// the complete list of headers as sent over the wire, including headers added by
364    /// the browser (e.g., `accept-encoding`, `accept-language`).
365    ///
366    /// # Errors
367    ///
368    /// Returns an error if the RPC call to the server fails.
369    ///
370    /// See: <https://playwright.dev/docs/api/class-request#request-headers-array>
371    pub async fn headers_array(&self) -> Result<Vec<HeaderEntry>> {
372        use serde::Deserialize;
373
374        #[derive(Deserialize)]
375        struct RawHeadersResponse {
376            headers: Vec<HeaderEntryRaw>,
377        }
378
379        #[derive(Deserialize)]
380        struct HeaderEntryRaw {
381            name: String,
382            value: String,
383        }
384
385        let result: RawHeadersResponse = self
386            .channel()
387            .send("rawRequestHeaders", serde_json::json!({}))
388            .await?;
389
390        Ok(result
391            .headers
392            .into_iter()
393            .map(|h| HeaderEntry {
394                name: h.name,
395                value: h.value,
396            })
397            .collect())
398    }
399
400    /// Returns all request headers as a `HashMap<String, String>` with lowercased keys.
401    ///
402    /// When multiple headers have the same name, their values are joined with `\n`
403    /// (matching Playwright's behavior).
404    ///
405    /// Sends a `"rawRequestHeaders"` RPC call to the Playwright server.
406    ///
407    /// # Errors
408    ///
409    /// Returns an error if the RPC call to the server fails.
410    ///
411    /// See: <https://playwright.dev/docs/api/class-request#request-all-headers>
412    pub async fn all_headers(&self) -> Result<HashMap<String, String>> {
413        let entries = self.headers_array().await?;
414        let mut map: HashMap<String, String> = HashMap::new();
415        for entry in entries {
416            let key = entry.name.to_lowercase();
417            map.entry(key)
418                .and_modify(|existing| {
419                    existing.push('\n');
420                    existing.push_str(&entry.value);
421                })
422                .or_insert(entry.value);
423        }
424        Ok(map)
425    }
426
427    /// Returns the value of the specified header (case-insensitive), or `None` if not found.
428    ///
429    /// Uses [`all_headers()`](Self::all_headers) internally, so it sends a
430    /// `"rawRequestHeaders"` RPC call to the Playwright server.
431    ///
432    /// # Errors
433    ///
434    /// Returns an error if the RPC call to the server fails.
435    ///
436    /// See: <https://playwright.dev/docs/api/class-request#request-header-value>
437    pub async fn header_value(&self, name: &str) -> Result<Option<String>> {
438        let all = self.all_headers().await?;
439        Ok(all.get(&name.to_lowercase()).cloned())
440    }
441
442    /// Returns timing information for the request.
443    ///
444    /// The timing data is sourced from the associated Response's initializer when the
445    /// `requestFinished` event fires. This method should be called from within a
446    /// `page.on_request_finished()` handler or after it has fired.
447    ///
448    /// Fields use `-1` to indicate that a timing phase was not reached or is
449    /// unavailable for a given request.
450    ///
451    /// # Errors
452    ///
453    /// Returns an error if timing data is not yet available (e.g., called before
454    /// `requestFinished` fires, or for a request that has not completed successfully).
455    ///
456    /// See: <https://playwright.dev/docs/api/class-request#request-timing>
457    pub async fn timing(&self) -> Result<ResourceTiming> {
458        use serde::Deserialize;
459
460        #[derive(Deserialize)]
461        #[serde(rename_all = "camelCase")]
462        struct RawTiming {
463            start_time: Option<f64>,
464            domain_lookup_start: Option<f64>,
465            domain_lookup_end: Option<f64>,
466            connect_start: Option<f64>,
467            connect_end: Option<f64>,
468            secure_connection_start: Option<f64>,
469            request_start: Option<f64>,
470            response_start: Option<f64>,
471            response_end: Option<f64>,
472        }
473
474        let timing_val = self.timing.lock().unwrap().clone().ok_or_else(|| {
475            crate::error::Error::ProtocolError(
476                "Request timing is not yet available. Call timing() from \
477                     on_request_finished() or after it has fired."
478                    .to_string(),
479            )
480        })?;
481
482        let raw: RawTiming = serde_json::from_value(timing_val).map_err(|e| {
483            crate::error::Error::ProtocolError(format!("Failed to parse timing data: {}", e))
484        })?;
485
486        Ok(ResourceTiming {
487            start_time: raw.start_time.unwrap_or(-1.0),
488            domain_lookup_start: raw.domain_lookup_start.unwrap_or(-1.0),
489            domain_lookup_end: raw.domain_lookup_end.unwrap_or(-1.0),
490            connect_start: raw.connect_start.unwrap_or(-1.0),
491            connect_end: raw.connect_end.unwrap_or(-1.0),
492            secure_connection_start: raw.secure_connection_start.unwrap_or(-1.0),
493            request_start: raw.request_start.unwrap_or(-1.0),
494            response_start: raw.response_start.unwrap_or(-1.0),
495            response_end: raw.response_end.unwrap_or(-1.0),
496        })
497    }
498}
499
500/// Resource timing information for an HTTP request.
501///
502/// All time values are in milliseconds relative to the navigation start.
503/// A value of `-1` indicates the timing phase was not reached.
504///
505/// See: <https://playwright.dev/docs/api/class-request#request-timing>
506#[derive(Debug, Clone)]
507pub struct ResourceTiming {
508    /// Request start time in milliseconds since epoch.
509    pub start_time: f64,
510    /// Time immediately before the browser starts the domain name lookup
511    /// for the resource. The value is given in milliseconds relative to
512    /// `startTime`, -1 if not available.
513    pub domain_lookup_start: f64,
514    /// Time immediately after the browser starts the domain name lookup
515    /// for the resource. The value is given in milliseconds relative to
516    /// `startTime`, -1 if not available.
517    pub domain_lookup_end: f64,
518    /// Time immediately before the user agent starts establishing the
519    /// connection to the server to retrieve the resource.
520    pub connect_start: f64,
521    /// Time immediately after the browser starts the handshake process
522    /// to secure the current connection.
523    pub secure_connection_start: f64,
524    /// Time immediately after the browser finishes establishing the connection
525    /// to the server to retrieve the resource.
526    pub connect_end: f64,
527    /// Time immediately before the browser starts requesting the resource from
528    /// the server, cache, or local resource.
529    pub request_start: f64,
530    /// Time immediately after the browser starts requesting the resource from
531    /// the server, cache, or local resource.
532    pub response_start: f64,
533    /// Time immediately after the browser receives the last byte of the resource
534    /// or immediately before the transport connection is closed, whichever comes first.
535    pub response_end: f64,
536}
537
538impl ChannelOwner for Request {
539    fn guid(&self) -> &str {
540        self.base.guid()
541    }
542
543    fn type_name(&self) -> &str {
544        self.base.type_name()
545    }
546
547    fn parent(&self) -> Option<Arc<dyn ChannelOwner>> {
548        self.base.parent()
549    }
550
551    fn connection(&self) -> Arc<dyn crate::server::connection::ConnectionLike> {
552        self.base.connection()
553    }
554
555    fn initializer(&self) -> &Value {
556        self.base.initializer()
557    }
558
559    fn channel(&self) -> &crate::server::channel::Channel {
560        self.base.channel()
561    }
562
563    fn dispose(&self, reason: crate::server::channel_owner::DisposeReason) {
564        self.base.dispose(reason)
565    }
566
567    fn adopt(&self, child: Arc<dyn ChannelOwner>) {
568        self.base.adopt(child)
569    }
570
571    fn add_child(&self, guid: Arc<str>, child: Arc<dyn ChannelOwner>) {
572        self.base.add_child(guid, child)
573    }
574
575    fn remove_child(&self, guid: &str) {
576        self.base.remove_child(guid)
577    }
578
579    fn on_event(&self, _method: &str, _params: Value) {
580        // Request events will be handled in future phases
581    }
582
583    fn was_collected(&self) -> bool {
584        self.base.was_collected()
585    }
586
587    fn as_any(&self) -> &dyn Any {
588        self
589    }
590}
591
592impl std::fmt::Debug for Request {
593    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
594        f.debug_struct("Request")
595            .field("guid", &self.guid())
596            .finish()
597    }
598}