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}