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