Skip to main content

playwright_rs/protocol/
api_request_context.rs

1// Copyright 2026 Paul Adamson
2// Licensed under the Apache License, Version 2.0
3//
4// APIRequestContext protocol object
5//
6// Enables performing HTTP requests without a browser, and is also used
7// by Route.fetch() to perform the actual network request before modification.
8//
9// See: https://playwright.dev/docs/api/class-apirequestcontext
10
11use crate::error::Result;
12use crate::protocol::route::FetchOptions;
13use crate::protocol::route::FetchResponse;
14use crate::server::channel::Channel;
15use crate::server::channel_owner::{
16    ChannelOwner, ChannelOwnerImpl, DisposeReason, ParentOrConnection,
17};
18use crate::server::connection::ConnectionLike;
19use serde::de::DeserializeOwned;
20use serde_json::{Value, json};
21use std::any::Any;
22use std::collections::HashMap;
23use std::sync::Arc;
24
25/// APIRequestContext provides methods for making HTTP requests.
26///
27/// This is the Playwright protocol object that performs actual HTTP operations.
28/// It is created automatically for each BrowserContext and can be accessed
29/// via `BrowserContext::request()`.
30///
31/// Used internally by `Route::fetch()` to perform the actual network request.
32///
33/// See: <https://playwright.dev/docs/api/class-apirequestcontext>
34#[derive(Clone)]
35pub struct APIRequestContext {
36    base: ChannelOwnerImpl,
37}
38
39impl APIRequestContext {
40    pub fn new(
41        parent: ParentOrConnection,
42        type_name: String,
43        guid: Arc<str>,
44        initializer: Value,
45    ) -> Result<Self> {
46        Ok(Self {
47            base: ChannelOwnerImpl::new(parent, type_name, guid, initializer),
48        })
49    }
50
51    /// Sends a GET request.
52    ///
53    /// See: <https://playwright.dev/docs/api/class-apirequestcontext#api-request-context-get>
54    pub async fn get(
55        &self,
56        url: &str,
57        options: impl Into<Option<FetchOptions>>,
58    ) -> Result<APIResponse> {
59        let options = options.into();
60        let mut opts = options.unwrap_or_default();
61        opts.method = Some("GET".to_string());
62        self.fetch(url, Some(opts)).await
63    }
64
65    /// Sends a POST request.
66    ///
67    /// See: <https://playwright.dev/docs/api/class-apirequestcontext#api-request-context-post>
68    pub async fn post(
69        &self,
70        url: &str,
71        options: impl Into<Option<FetchOptions>>,
72    ) -> Result<APIResponse> {
73        let options = options.into();
74        let mut opts = options.unwrap_or_default();
75        opts.method = Some("POST".to_string());
76        self.fetch(url, Some(opts)).await
77    }
78
79    /// Sends a PUT request.
80    ///
81    /// See: <https://playwright.dev/docs/api/class-apirequestcontext#api-request-context-put>
82    pub async fn put(
83        &self,
84        url: &str,
85        options: impl Into<Option<FetchOptions>>,
86    ) -> Result<APIResponse> {
87        let options = options.into();
88        let mut opts = options.unwrap_or_default();
89        opts.method = Some("PUT".to_string());
90        self.fetch(url, Some(opts)).await
91    }
92
93    /// Sends a DELETE request.
94    ///
95    /// See: <https://playwright.dev/docs/api/class-apirequestcontext#api-request-context-delete>
96    pub async fn delete(
97        &self,
98        url: &str,
99        options: impl Into<Option<FetchOptions>>,
100    ) -> Result<APIResponse> {
101        let options = options.into();
102        let mut opts = options.unwrap_or_default();
103        opts.method = Some("DELETE".to_string());
104        self.fetch(url, Some(opts)).await
105    }
106
107    /// Sends a PATCH request.
108    ///
109    /// See: <https://playwright.dev/docs/api/class-apirequestcontext#api-request-context-patch>
110    pub async fn patch(
111        &self,
112        url: &str,
113        options: impl Into<Option<FetchOptions>>,
114    ) -> Result<APIResponse> {
115        let options = options.into();
116        let mut opts = options.unwrap_or_default();
117        opts.method = Some("PATCH".to_string());
118        self.fetch(url, Some(opts)).await
119    }
120
121    /// Sends a HEAD request.
122    ///
123    /// See: <https://playwright.dev/docs/api/class-apirequestcontext#api-request-context-head>
124    pub async fn head(
125        &self,
126        url: &str,
127        options: impl Into<Option<FetchOptions>>,
128    ) -> Result<APIResponse> {
129        let options = options.into();
130        let mut opts = options.unwrap_or_default();
131        opts.method = Some("HEAD".to_string());
132        self.fetch(url, Some(opts)).await
133    }
134
135    /// Sends a fetch request with the given options, returning an `APIResponse`.
136    ///
137    /// This is the public-facing fetch method that returns a lazy `APIResponse`.
138    /// The response body is not fetched until `body()`, `text()`, or `json()` is called.
139    ///
140    /// See: <https://playwright.dev/docs/api/class-apirequestcontext#api-request-context-fetch>
141    pub async fn fetch(
142        &self,
143        url: &str,
144        options: impl Into<Option<FetchOptions>>,
145    ) -> Result<APIResponse> {
146        let options = options.into();
147        let opts = options.unwrap_or_default();
148
149        let mut params = json!({
150            "url": url,
151            "timeout": opts.timeout.unwrap_or(crate::DEFAULT_TIMEOUT_MS)
152        });
153
154        if let Some(method) = opts.method {
155            params["method"] = json!(method);
156        }
157        if let Some(headers) = opts.headers {
158            params["headers"] = Value::Array(crate::protocol::route_params::header_array(headers));
159        }
160        if let Some(encoded) =
161            crate::protocol::route_params::post_data_param(opts.post_data, opts.post_data_bytes)
162        {
163            params["postData"] = Value::String(encoded);
164        }
165        if let Some(max_redirects) = opts.max_redirects {
166            params["maxRedirects"] = json!(max_redirects);
167        }
168        if let Some(max_retries) = opts.max_retries {
169            params["maxRetries"] = json!(max_retries);
170        }
171
172        #[derive(serde::Deserialize)]
173        struct FetchResult {
174            response: ApiResponseData,
175        }
176
177        #[derive(serde::Deserialize)]
178        #[serde(rename_all = "camelCase")]
179        struct ApiResponseData {
180            fetch_uid: String,
181            url: String,
182            status: u16,
183            status_text: String,
184            headers: Vec<HeaderEntry>,
185            #[serde(default)]
186            security_details: Option<crate::protocol::response::SecurityDetails>,
187            #[serde(default)]
188            server_addr: Option<crate::protocol::response::RemoteAddr>,
189            #[serde(default)]
190            timing: Option<serde_json::Value>,
191            #[serde(default)]
192            response_end_timing: Option<f64>,
193        }
194
195        #[derive(serde::Deserialize)]
196        struct HeaderEntry {
197            name: String,
198            value: String,
199        }
200
201        let result: FetchResult = self.base.channel().send("fetch", params).await?;
202
203        let headers: HashMap<String, String> = result
204            .response
205            .headers
206            .into_iter()
207            .map(|h| (h.name, h.value))
208            .collect();
209
210        Ok(APIResponse {
211            context: self.clone(),
212            url: result.response.url,
213            status: result.response.status,
214            status_text: result.response.status_text,
215            headers,
216            fetch_uid: result.response.fetch_uid,
217            security_details: result.response.security_details,
218            server_addr: result.response.server_addr,
219            timing: result.response.timing.clone().and_then(|mut t| {
220                crate::protocol::ResourceTiming::merge_response_end(
221                    &mut t,
222                    result.response.response_end_timing,
223                );
224                crate::protocol::ResourceTiming::from_protocol(&t)
225            }),
226            response_end_timing: result.response.response_end_timing,
227        })
228    }
229
230    /// Disposes this `APIRequestContext`, freeing server resources.
231    ///
232    /// After calling `dispose()`, the context cannot be used for further requests.
233    ///
234    /// See: <https://playwright.dev/docs/api/class-apirequestcontext#api-request-context-dispose>
235    pub async fn dispose(&self) -> Result<()> {
236        self.base
237            .channel()
238            .send_no_result("dispose", json!({}))
239            .await
240    }
241
242    /// Performs an HTTP fetch request and returns the response.
243    ///
244    /// This is the internal method used by `Route::fetch()`. It sends the request
245    /// via the Playwright server and returns the response with headers and body.
246    ///
247    /// # Arguments
248    ///
249    /// * `url` - The URL to fetch
250    /// * `options` - Optional parameters to customize the request
251    ///
252    /// See: <https://playwright.dev/docs/api/class-apirequestcontext#api-request-context-fetch>
253    pub(crate) async fn inner_fetch(
254        &self,
255        url: &str,
256        options: Option<InnerFetchOptions>,
257    ) -> Result<FetchResponse> {
258        let opts = options.unwrap_or_default();
259
260        let mut params = json!({
261            "url": url,
262            "timeout": opts.timeout.unwrap_or(crate::DEFAULT_TIMEOUT_MS)
263        });
264
265        if let Some(method) = opts.method {
266            params["method"] = json!(method);
267        }
268        if let Some(headers) = opts.headers {
269            params["headers"] = Value::Array(crate::protocol::route_params::header_array(headers));
270        }
271        if let Some(encoded) =
272            crate::protocol::route_params::post_data_param(opts.post_data, opts.post_data_bytes)
273        {
274            params["postData"] = Value::String(encoded);
275        }
276        if let Some(max_redirects) = opts.max_redirects {
277            params["maxRedirects"] = json!(max_redirects);
278        }
279        if let Some(max_retries) = opts.max_retries {
280            params["maxRetries"] = json!(max_retries);
281        }
282
283        // Call the fetch command on APIRequestContext channel
284        #[derive(serde::Deserialize)]
285        struct FetchResult {
286            response: ApiResponseData,
287        }
288
289        #[derive(serde::Deserialize)]
290        #[serde(rename_all = "camelCase")]
291        struct ApiResponseData {
292            fetch_uid: String,
293            #[allow(dead_code)]
294            url: String,
295            status: u16,
296            status_text: String,
297            headers: Vec<HeaderEntry>,
298        }
299
300        #[derive(serde::Deserialize)]
301        struct HeaderEntry {
302            name: String,
303            value: String,
304        }
305
306        let result: FetchResult = self.base.channel().send("fetch", params).await?;
307
308        // Now fetch the response body using fetchResponseBody
309        let body = self.fetch_response_body(&result.response.fetch_uid).await?;
310
311        // Dispose the API response to free server resources
312        let _ = self.dispose_api_response(&result.response.fetch_uid).await;
313
314        Ok(FetchResponse {
315            status: result.response.status,
316            status_text: result.response.status_text,
317            headers: result
318                .response
319                .headers
320                .into_iter()
321                .map(|h| (h.name, h.value))
322                .collect(),
323            body,
324        })
325    }
326
327    /// Fetches the response body for a given fetch UID.
328    async fn fetch_response_body(&self, fetch_uid: &str) -> Result<Vec<u8>> {
329        #[derive(serde::Deserialize)]
330        struct BodyResult {
331            #[serde(default)]
332            binary: Option<String>,
333        }
334
335        let result: BodyResult = self
336            .base
337            .channel()
338            .send("fetchResponseBody", json!({ "fetchUid": fetch_uid }))
339            .await?;
340
341        match result.binary {
342            Some(encoded) if !encoded.is_empty() => {
343                use base64::Engine;
344                base64::engine::general_purpose::STANDARD
345                    .decode(&encoded)
346                    .map_err(|e| {
347                        crate::error::Error::ProtocolError(format!(
348                            "Failed to decode response body: {}",
349                            e
350                        ))
351                    })
352            }
353            _ => Ok(vec![]),
354        }
355    }
356
357    /// Disposes an API response to free server resources.
358    async fn dispose_api_response(&self, fetch_uid: &str) -> Result<()> {
359        self.base
360            .channel()
361            .send_no_result("disposeAPIResponse", json!({ "fetchUid": fetch_uid }))
362            .await
363    }
364}
365
366/// A lazy HTTP response returned by `APIRequestContext` methods.
367///
368/// Unlike [`crate::protocol::route::FetchResponse`] (which eagerly fetches the body),
369/// `APIResponse` holds a `fetch_uid` and fetches the body on demand.
370///
371/// See: <https://playwright.dev/docs/api/class-apiresponse>
372#[derive(Clone)]
373pub struct APIResponse {
374    context: APIRequestContext,
375    url: String,
376    status: u16,
377    status_text: String,
378    headers: HashMap<String, String>,
379    fetch_uid: String,
380    security_details: Option<crate::protocol::response::SecurityDetails>,
381    server_addr: Option<crate::protocol::response::RemoteAddr>,
382    timing: Option<crate::protocol::ResourceTiming>,
383    response_end_timing: Option<f64>,
384}
385
386impl APIResponse {
387    /// Returns the URL of the response.
388    pub fn url(&self) -> &str {
389        &self.url
390    }
391
392    /// Returns the HTTP status code.
393    pub fn status(&self) -> u16 {
394        self.status
395    }
396
397    /// Returns the HTTP status text (e.g., "OK", "Not Found").
398    pub fn status_text(&self) -> &str {
399        &self.status_text
400    }
401
402    /// Returns `true` if the status code is in the 200–299 range.
403    pub fn ok(&self) -> bool {
404        (200..300).contains(&self.status)
405    }
406
407    /// Returns the response headers as a `HashMap<String, String>`.
408    pub fn headers(&self) -> &HashMap<String, String> {
409        &self.headers
410    }
411
412    /// Returns resource timing for this response, or `None` if the server did
413    /// not report any.
414    ///
415    /// Mirrors the browser-side [`Request::timing`](crate::protocol::Request::timing),
416    /// and reuses the same type, so the phases mean the same thing on both
417    /// sides: milliseconds relative to `start_time`, with `-1` for a phase
418    /// that was not reached.
419    ///
420    /// Unlike the browser-side accessor this is synchronous and always
421    /// available, because the driver sends it in the response itself rather
422    /// than after a later event.
423    ///
424    /// See: <https://playwright.dev/docs/api/class-apiresponse#api-response-timing>
425    pub fn timing(&self) -> Option<&crate::protocol::ResourceTiming> {
426        self.timing.as_ref()
427    }
428
429    /// Returns the time at which the response finished, in milliseconds
430    /// relative to the timing's `start_time`, or `None` if unavailable.
431    pub fn response_end_timing(&self) -> Option<f64> {
432        self.response_end_timing
433    }
434
435    /// Returns TLS/SSL security details for HTTPS responses, or `None` for
436    /// plain HTTP. Mirrors the browser-side `Response::security_details`.
437    ///
438    /// See: <https://playwright.dev/docs/api/class-apiresponse#api-response-security-details>
439    pub fn security_details(&self) -> Option<&crate::protocol::response::SecurityDetails> {
440        self.security_details.as_ref()
441    }
442
443    /// Returns the server's resolved IP address and port for this response,
444    /// or `None` if unavailable.
445    ///
446    /// See: <https://playwright.dev/docs/api/class-apiresponse#api-response-server-addr>
447    pub fn server_addr(&self) -> Option<&crate::protocol::response::RemoteAddr> {
448        self.server_addr.as_ref()
449    }
450
451    /// Fetches and returns the response body as bytes.
452    ///
453    /// See: <https://playwright.dev/docs/api/class-apiresponse#api-response-body>
454    pub async fn body(&self) -> Result<Vec<u8>> {
455        self.context.fetch_response_body(&self.fetch_uid).await
456    }
457
458    /// Fetches and returns the response body as a UTF-8 string.
459    ///
460    /// See: <https://playwright.dev/docs/api/class-apiresponse#api-response-text>
461    pub async fn text(&self) -> Result<String> {
462        let bytes = self.body().await?;
463        String::from_utf8(bytes).map_err(|e| {
464            crate::error::Error::ProtocolError(format!("Response body is not valid UTF-8: {}", e))
465        })
466    }
467
468    /// Fetches the response body and deserializes it as JSON.
469    ///
470    /// See: <https://playwright.dev/docs/api/class-apiresponse#api-response-json>
471    pub async fn json<T: DeserializeOwned>(&self) -> Result<T> {
472        let bytes = self.body().await?;
473        serde_json::from_slice(&bytes).map_err(|e| {
474            crate::error::Error::ProtocolError(format!("Failed to parse response JSON: {}", e))
475        })
476    }
477
478    /// Disposes this response, freeing server-side resources for the response body.
479    ///
480    /// See: <https://playwright.dev/docs/api/class-apiresponse#api-response-dispose>
481    pub async fn dispose(&self) -> Result<()> {
482        self.context.dispose_api_response(&self.fetch_uid).await
483    }
484}
485
486impl std::fmt::Debug for APIResponse {
487    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
488        f.debug_struct("APIResponse")
489            .field("url", &self.url)
490            .field("status", &self.status)
491            .field("status_text", &self.status_text)
492            .finish()
493    }
494}
495
496/// Options for creating a new `APIRequestContext` via `APIRequest::new_context()`.
497///
498/// See: <https://playwright.dev/docs/api/class-apirequest#api-request-new-context>
499#[derive(Debug, Clone, Default)]
500#[non_exhaustive]
501pub struct APIRequestContextOptions {
502    /// Base URL for all relative requests made with this context.
503    pub base_url: Option<String>,
504    /// Extra HTTP headers to be sent with every request.
505    pub extra_http_headers: Option<HashMap<String, String>>,
506    /// Whether to ignore HTTPS errors when making requests.
507    pub ignore_https_errors: Option<bool>,
508    /// User agent string to send with requests.
509    pub user_agent: Option<String>,
510    /// Default timeout for fetch operations in milliseconds.
511    pub timeout: Option<f64>,
512    /// Credentials for HTTP authentication, matched per request origin.
513    ///
514    /// This is the one context that honors
515    /// [`HttpCredentialsSend::Always`](crate::protocol::HttpCredentialsSend).
516    pub http_credentials: Option<Vec<crate::protocol::HttpCredentials>>,
517}
518
519impl APIRequestContextOptions {
520    /// Base URL prepended to relative request paths.
521    pub fn base_url(mut self, base_url: impl Into<String>) -> Self {
522        self.base_url = Some(base_url.into());
523        self
524    }
525    /// Extra HTTP headers sent with every request.
526    pub fn extra_http_headers(mut self, headers: HashMap<String, String>) -> Self {
527        self.extra_http_headers = Some(headers);
528        self
529    }
530    /// Ignore HTTPS certificate errors.
531    pub fn ignore_https_errors(mut self, ignore: bool) -> Self {
532        self.ignore_https_errors = Some(ignore);
533        self
534    }
535    /// Credentials for HTTP authentication, matched per request origin.
536    pub fn http_credentials(mut self, credentials: Vec<crate::protocol::HttpCredentials>) -> Self {
537        self.http_credentials = Some(credentials);
538        self
539    }
540    /// User-Agent header value.
541    pub fn user_agent(mut self, user_agent: impl Into<String>) -> Self {
542        self.user_agent = Some(user_agent.into());
543        self
544    }
545    /// Maximum time in milliseconds for each request.
546    pub fn timeout(mut self, timeout: f64) -> Self {
547        self.timeout = Some(timeout);
548        self
549    }
550}
551
552/// Factory for creating standalone `APIRequestContext` instances.
553///
554/// Obtained via `playwright.request()`. Use `new_context()` to create a context
555/// for making HTTP requests outside of a browser page.
556///
557/// `APIRequest` intentionally holds only the channel and connection reference,
558/// NOT a `Playwright` clone. Holding a `Playwright` clone would trigger the
559/// server shutdown Drop impl when the temporary `APIRequest` is dropped.
560///
561/// # Example
562///
563/// ```no_run
564/// use playwright_rs::protocol::Playwright;
565///
566/// #[tokio::main]
567/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
568///     let playwright = Playwright::launch().await?;
569///
570///     let ctx = playwright.request().new_context(None).await?;
571///     let response = ctx.get("https://example.com/api/data", None).await?;
572///     assert!(response.ok());
573///     let body = response.text().await?;
574///
575///     ctx.dispose().await?;
576///     playwright.shutdown().await?;
577///     Ok(())
578/// }
579/// ```
580///
581/// See: <https://playwright.dev/docs/api/class-apirequest>
582pub struct APIRequest {
583    channel: crate::server::channel::Channel,
584    connection: Arc<dyn ConnectionLike>,
585}
586
587impl APIRequest {
588    pub(crate) fn new(
589        channel: crate::server::channel::Channel,
590        connection: Arc<dyn ConnectionLike>,
591    ) -> Self {
592        Self {
593            channel,
594            connection,
595        }
596    }
597
598    /// Creates a new `APIRequestContext` for making HTTP requests.
599    ///
600    /// # Arguments
601    ///
602    /// * `options` — Optional configuration for the new context
603    ///
604    /// See: <https://playwright.dev/docs/api/class-apirequest#api-request-new-context>
605    pub async fn new_context(
606        &self,
607        options: impl Into<Option<APIRequestContextOptions>>,
608    ) -> Result<APIRequestContext> {
609        use crate::server::connection::ConnectionExt;
610
611        let options = options.into();
612        let mut params = json!({});
613
614        if let Some(opts) = options {
615            if let Some(base_url) = opts.base_url {
616                params["baseURL"] = json!(base_url);
617            }
618            if let Some(headers) = opts.extra_http_headers {
619                params["extraHTTPHeaders"] =
620                    Value::Array(crate::protocol::route_params::header_array(headers));
621            }
622            if let Some(ignore) = opts.ignore_https_errors {
623                params["ignoreHTTPSErrors"] = json!(ignore);
624            }
625            if let Some(ua) = opts.user_agent {
626                params["userAgent"] = json!(ua);
627            }
628            if let Some(credentials) = opts.http_credentials {
629                params["httpCredentials"] = json!(credentials);
630            }
631            if let Some(timeout) = opts.timeout {
632                params["timeout"] = json!(timeout);
633            }
634        }
635
636        #[derive(serde::Deserialize)]
637        struct NewRequestResult {
638            request: GuidRef,
639        }
640
641        #[derive(serde::Deserialize)]
642        struct GuidRef {
643            guid: String,
644        }
645
646        let result: NewRequestResult = self.channel.send("newRequest", params).await?;
647
648        self.connection
649            .get_typed::<APIRequestContext>(&result.request.guid)
650            .await
651    }
652}
653
654impl std::fmt::Debug for APIRequest {
655    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
656        f.debug_struct("APIRequest").finish()
657    }
658}
659
660/// Options for APIRequestContext.inner_fetch()
661#[derive(Debug, Clone, Default)]
662pub(crate) struct InnerFetchOptions {
663    pub method: Option<String>,
664    pub headers: Option<std::collections::HashMap<String, String>>,
665    pub post_data: Option<String>,
666    pub post_data_bytes: Option<Vec<u8>>,
667    pub max_redirects: Option<u32>,
668    pub max_retries: Option<u32>,
669    pub timeout: Option<f64>,
670}
671
672impl ChannelOwner for APIRequestContext {
673    fn guid(&self) -> &str {
674        self.base.guid()
675    }
676
677    fn type_name(&self) -> &str {
678        self.base.type_name()
679    }
680
681    fn parent(&self) -> Option<Arc<dyn ChannelOwner>> {
682        self.base.parent()
683    }
684
685    fn connection(&self) -> Arc<dyn ConnectionLike> {
686        self.base.connection()
687    }
688
689    fn initializer(&self) -> &Value {
690        self.base.initializer()
691    }
692
693    fn channel(&self) -> &Channel {
694        self.base.channel()
695    }
696
697    fn dispose(&self, reason: DisposeReason) {
698        self.base.dispose(reason)
699    }
700
701    fn adopt(&self, child: Arc<dyn ChannelOwner>) {
702        self.base.adopt(child)
703    }
704
705    fn add_child(&self, guid: Arc<str>, child: Arc<dyn ChannelOwner>) {
706        self.base.add_child(guid, child)
707    }
708
709    fn remove_child(&self, guid: &str) {
710        self.base.remove_child(guid)
711    }
712
713    fn on_event(&self, method: &str, params: Value) {
714        self.base.on_event(method, params)
715    }
716
717    fn was_collected(&self) -> bool {
718        self.base.was_collected()
719    }
720
721    fn as_any(&self) -> &dyn Any {
722        self
723    }
724}
725
726impl std::fmt::Debug for APIRequestContext {
727    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
728        f.debug_struct("APIRequestContext")
729            .field("guid", &self.guid())
730            .finish()
731    }
732}