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}