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