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