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