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