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}