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}
30
31impl Request {
32    /// Creates a new Request from protocol initialization
33    ///
34    /// This is called by the object factory when the server sends a `__create__` message
35    /// for a Request object.
36    pub fn new(
37        parent: Arc<dyn ChannelOwner>,
38        type_name: String,
39        guid: Arc<str>,
40        initializer: Value,
41    ) -> Result<Self> {
42        let base = ChannelOwnerImpl::new(
43            ParentOrConnection::Parent(parent),
44            type_name,
45            guid,
46            initializer,
47        );
48
49        Ok(Self {
50            base,
51            failure_text: Arc::new(Mutex::new(None)),
52            timing: Arc::new(Mutex::new(None)),
53        })
54    }
55
56    /// Returns the URL of the request.
57    ///
58    /// See: <https://playwright.dev/docs/api/class-request#request-url>
59    pub fn url(&self) -> &str {
60        self.initializer()
61            .get("url")
62            .and_then(|v| v.as_str())
63            .unwrap_or("")
64    }
65
66    /// Returns the HTTP method of the request (GET, POST, etc.).
67    ///
68    /// See: <https://playwright.dev/docs/api/class-request#request-method>
69    pub fn method(&self) -> &str {
70        self.initializer()
71            .get("method")
72            .and_then(|v| v.as_str())
73            .unwrap_or("GET")
74    }
75
76    /// Returns the resource type of the request (e.g., "document", "stylesheet", "image", "fetch", etc.).
77    ///
78    /// See: <https://playwright.dev/docs/api/class-request#request-resource-type>
79    pub fn resource_type(&self) -> &str {
80        self.initializer()
81            .get("resourceType")
82            .and_then(|v| v.as_str())
83            .unwrap_or("other")
84    }
85
86    /// Check if this request is for a navigation (main document).
87    ///
88    /// A navigation request is when the request is for the main frame's document.
89    /// This is used to distinguish between main document loads and subresource loads.
90    ///
91    /// See: <https://playwright.dev/docs/api/class-request#request-is-navigation-request>
92    pub fn is_navigation_request(&self) -> bool {
93        self.resource_type() == "document"
94    }
95
96    /// Returns the request headers as a HashMap.
97    ///
98    /// The headers are read from the protocol initializer data. The format in the
99    /// protocol is a list of `{name, value}` objects which are merged into a
100    /// `HashMap<String, String>`. If duplicate header names exist, the last
101    /// value wins.
102    ///
103    /// For the full set of raw headers (including duplicates), use
104    /// [`headers_array()`](Self::headers_array) or [`all_headers()`](Self::all_headers).
105    ///
106    /// See: <https://playwright.dev/docs/api/class-request#request-headers>
107    pub fn headers(&self) -> HashMap<String, String> {
108        let mut map = HashMap::new();
109        if let Some(headers) = self.initializer().get("headers").and_then(|v| v.as_array()) {
110            for entry in headers {
111                if let (Some(name), Some(value)) = (
112                    entry.get("name").and_then(|v| v.as_str()),
113                    entry.get("value").and_then(|v| v.as_str()),
114                ) {
115                    map.insert(name.to_lowercase(), value.to_string());
116                }
117            }
118        }
119        map
120    }
121
122    /// Returns the raw base64-encoded post data from the initializer, or `None`.
123    fn post_data_b64(&self) -> Option<&str> {
124        self.initializer().get("postData").and_then(|v| v.as_str())
125    }
126
127    /// Returns the request body (POST data) as bytes, or `None` if there is no body.
128    ///
129    /// The Playwright protocol sends `postData` as a base64-encoded string.
130    /// This method decodes it to raw bytes.
131    ///
132    /// This is a local read and does not require an RPC call.
133    ///
134    /// See: <https://playwright.dev/docs/api/class-request#request-post-data-buffer>
135    pub fn post_data_buffer(&self) -> Option<Vec<u8>> {
136        let b64 = self.post_data_b64()?;
137        use base64::Engine;
138        base64::engine::general_purpose::STANDARD.decode(b64).ok()
139    }
140
141    /// Returns the request body (POST data) as a UTF-8 string, or `None` if there is no body.
142    ///
143    /// The Playwright protocol sends `postData` as a base64-encoded string.
144    /// This method decodes the base64 and then converts the bytes to a UTF-8 string.
145    ///
146    /// This is a local read and does not require an RPC call.
147    ///
148    /// See: <https://playwright.dev/docs/api/class-request#request-post-data>
149    pub fn post_data(&self) -> Option<String> {
150        let bytes = self.post_data_buffer()?;
151        String::from_utf8(bytes).ok()
152    }
153
154    /// Parses the POST data as JSON and deserializes into the target type `T`.
155    ///
156    /// Returns `None` if the request has no POST data, or `Some(Err(...))` if the
157    /// JSON parsing fails.
158    ///
159    /// See: <https://playwright.dev/docs/api/class-request#request-post-data-json>
160    pub fn post_data_json<T: DeserializeOwned>(&self) -> Option<Result<T>> {
161        let data = self.post_data()?;
162        Some(serde_json::from_str(&data).map_err(|e| {
163            crate::error::Error::ProtocolError(format!(
164                "Failed to parse request post data as JSON: {}",
165                e
166            ))
167        }))
168    }
169
170    /// Returns the error text if the request failed, or `None` for successful requests.
171    ///
172    /// The failure text is set when the `requestFailed` browser event fires for this
173    /// request. Use `page.on_request_failed()` to capture failed requests and then
174    /// call this method to get the error reason.
175    ///
176    /// See: <https://playwright.dev/docs/api/class-request#request-failure>
177    pub fn failure(&self) -> Option<String> {
178        self.failure_text.lock().unwrap().clone()
179    }
180
181    /// Sets the failure text. Called by the dispatcher when a `requestFailed` event
182    /// arrives for this request.
183    pub(crate) fn set_failure_text(&self, text: String) {
184        *self.failure_text.lock().unwrap() = Some(text);
185    }
186
187    /// Sets the timing data. Called by the dispatcher when a `requestFinished` event
188    /// arrives and timing data is extracted from the associated Response's initializer.
189    pub(crate) fn set_timing(&self, timing_val: Value) {
190        *self.timing.lock().unwrap() = Some(timing_val);
191    }
192
193    /// Returns all request headers as name-value pairs, preserving duplicates.
194    ///
195    /// Sends a `"rawRequestHeaders"` RPC call to the Playwright server which returns
196    /// the complete list of headers as sent over the wire, including headers added by
197    /// the browser (e.g., `accept-encoding`, `accept-language`).
198    ///
199    /// # Errors
200    ///
201    /// Returns an error if the RPC call to the server fails.
202    ///
203    /// See: <https://playwright.dev/docs/api/class-request#request-headers-array>
204    pub async fn headers_array(&self) -> Result<Vec<HeaderEntry>> {
205        use serde::Deserialize;
206
207        #[derive(Deserialize)]
208        struct RawHeadersResponse {
209            headers: Vec<HeaderEntryRaw>,
210        }
211
212        #[derive(Deserialize)]
213        struct HeaderEntryRaw {
214            name: String,
215            value: String,
216        }
217
218        let result: RawHeadersResponse = self
219            .channel()
220            .send("rawRequestHeaders", serde_json::json!({}))
221            .await?;
222
223        Ok(result
224            .headers
225            .into_iter()
226            .map(|h| HeaderEntry {
227                name: h.name,
228                value: h.value,
229            })
230            .collect())
231    }
232
233    /// Returns all request headers as a `HashMap<String, String>` with lowercased keys.
234    ///
235    /// When multiple headers have the same name, their values are joined with `\n`
236    /// (matching Playwright's behavior).
237    ///
238    /// Sends a `"rawRequestHeaders"` RPC call to the Playwright server.
239    ///
240    /// # Errors
241    ///
242    /// Returns an error if the RPC call to the server fails.
243    ///
244    /// See: <https://playwright.dev/docs/api/class-request#request-all-headers>
245    pub async fn all_headers(&self) -> Result<HashMap<String, String>> {
246        let entries = self.headers_array().await?;
247        let mut map: HashMap<String, String> = HashMap::new();
248        for entry in entries {
249            let key = entry.name.to_lowercase();
250            map.entry(key)
251                .and_modify(|existing| {
252                    existing.push('\n');
253                    existing.push_str(&entry.value);
254                })
255                .or_insert(entry.value);
256        }
257        Ok(map)
258    }
259
260    /// Returns the value of the specified header (case-insensitive), or `None` if not found.
261    ///
262    /// Uses [`all_headers()`](Self::all_headers) internally, so it sends a
263    /// `"rawRequestHeaders"` RPC call to the Playwright server.
264    ///
265    /// # Errors
266    ///
267    /// Returns an error if the RPC call to the server fails.
268    ///
269    /// See: <https://playwright.dev/docs/api/class-request#request-header-value>
270    pub async fn header_value(&self, name: &str) -> Result<Option<String>> {
271        let all = self.all_headers().await?;
272        Ok(all.get(&name.to_lowercase()).cloned())
273    }
274
275    /// Returns timing information for the request.
276    ///
277    /// The timing data is sourced from the associated Response's initializer when the
278    /// `requestFinished` event fires. This method should be called from within a
279    /// `page.on_request_finished()` handler or after it has fired.
280    ///
281    /// Fields use `-1` to indicate that a timing phase was not reached or is
282    /// unavailable for a given request.
283    ///
284    /// # Errors
285    ///
286    /// Returns an error if timing data is not yet available (e.g., called before
287    /// `requestFinished` fires, or for a request that has not completed successfully).
288    ///
289    /// See: <https://playwright.dev/docs/api/class-request#request-timing>
290    pub async fn timing(&self) -> Result<ResourceTiming> {
291        use serde::Deserialize;
292
293        #[derive(Deserialize)]
294        #[serde(rename_all = "camelCase")]
295        struct RawTiming {
296            start_time: Option<f64>,
297            domain_lookup_start: Option<f64>,
298            domain_lookup_end: Option<f64>,
299            connect_start: Option<f64>,
300            connect_end: Option<f64>,
301            secure_connection_start: Option<f64>,
302            request_start: Option<f64>,
303            response_start: Option<f64>,
304            response_end: Option<f64>,
305        }
306
307        let timing_val = self.timing.lock().unwrap().clone().ok_or_else(|| {
308            crate::error::Error::ProtocolError(
309                "Request timing is not yet available. Call timing() from \
310                     on_request_finished() or after it has fired."
311                    .to_string(),
312            )
313        })?;
314
315        let raw: RawTiming = serde_json::from_value(timing_val).map_err(|e| {
316            crate::error::Error::ProtocolError(format!("Failed to parse timing data: {}", e))
317        })?;
318
319        Ok(ResourceTiming {
320            start_time: raw.start_time.unwrap_or(-1.0),
321            domain_lookup_start: raw.domain_lookup_start.unwrap_or(-1.0),
322            domain_lookup_end: raw.domain_lookup_end.unwrap_or(-1.0),
323            connect_start: raw.connect_start.unwrap_or(-1.0),
324            connect_end: raw.connect_end.unwrap_or(-1.0),
325            secure_connection_start: raw.secure_connection_start.unwrap_or(-1.0),
326            request_start: raw.request_start.unwrap_or(-1.0),
327            response_start: raw.response_start.unwrap_or(-1.0),
328            response_end: raw.response_end.unwrap_or(-1.0),
329        })
330    }
331}
332
333/// Resource timing information for an HTTP request.
334///
335/// All time values are in milliseconds relative to the navigation start.
336/// A value of `-1` indicates the timing phase was not reached.
337///
338/// See: <https://playwright.dev/docs/api/class-request#request-timing>
339#[derive(Debug, Clone)]
340pub struct ResourceTiming {
341    /// Request start time in milliseconds since epoch.
342    pub start_time: f64,
343    /// Time immediately before the browser starts the domain name lookup
344    /// for the resource. The value is given in milliseconds relative to
345    /// `startTime`, -1 if not available.
346    pub domain_lookup_start: f64,
347    /// Time immediately after the browser starts the domain name lookup
348    /// for the resource. The value is given in milliseconds relative to
349    /// `startTime`, -1 if not available.
350    pub domain_lookup_end: f64,
351    /// Time immediately before the user agent starts establishing the
352    /// connection to the server to retrieve the resource.
353    pub connect_start: f64,
354    /// Time immediately after the browser starts the handshake process
355    /// to secure the current connection.
356    pub secure_connection_start: f64,
357    /// Time immediately after the browser finishes establishing the connection
358    /// to the server to retrieve the resource.
359    pub connect_end: f64,
360    /// Time immediately before the browser starts requesting the resource from
361    /// the server, cache, or local resource.
362    pub request_start: f64,
363    /// Time immediately after the browser starts requesting the resource from
364    /// the server, cache, or local resource.
365    pub response_start: f64,
366    /// Time immediately after the browser receives the last byte of the resource
367    /// or immediately before the transport connection is closed, whichever comes first.
368    pub response_end: f64,
369}
370
371impl ChannelOwner for Request {
372    fn guid(&self) -> &str {
373        self.base.guid()
374    }
375
376    fn type_name(&self) -> &str {
377        self.base.type_name()
378    }
379
380    fn parent(&self) -> Option<Arc<dyn ChannelOwner>> {
381        self.base.parent()
382    }
383
384    fn connection(&self) -> Arc<dyn crate::server::connection::ConnectionLike> {
385        self.base.connection()
386    }
387
388    fn initializer(&self) -> &Value {
389        self.base.initializer()
390    }
391
392    fn channel(&self) -> &crate::server::channel::Channel {
393        self.base.channel()
394    }
395
396    fn dispose(&self, reason: crate::server::channel_owner::DisposeReason) {
397        self.base.dispose(reason)
398    }
399
400    fn adopt(&self, child: Arc<dyn ChannelOwner>) {
401        self.base.adopt(child)
402    }
403
404    fn add_child(&self, guid: Arc<str>, child: Arc<dyn ChannelOwner>) {
405        self.base.add_child(guid, child)
406    }
407
408    fn remove_child(&self, guid: &str) {
409        self.base.remove_child(guid)
410    }
411
412    fn on_event(&self, _method: &str, _params: Value) {
413        // Request events will be handled in future phases
414    }
415
416    fn was_collected(&self) -> bool {
417        self.base.was_collected()
418    }
419
420    fn as_any(&self) -> &dyn Any {
421        self
422    }
423}
424
425impl std::fmt::Debug for Request {
426    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
427        f.debug_struct("Request")
428            .field("guid", &self.guid())
429            .finish()
430    }
431}