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            let headers_array: Vec<Value> = headers
159                .into_iter()
160                .map(|(name, value)| json!({"name": name, "value": value}))
161                .collect();
162            params["headers"] = json!(headers_array);
163        }
164        if let Some(post_data) = opts.post_data {
165            use base64::Engine;
166            let encoded = base64::engine::general_purpose::STANDARD.encode(post_data.as_bytes());
167            params["postData"] = json!(encoded);
168        } else if let Some(post_data_bytes) = opts.post_data_bytes {
169            use base64::Engine;
170            let encoded = base64::engine::general_purpose::STANDARD.encode(&post_data_bytes);
171            params["postData"] = json!(encoded);
172        }
173        if let Some(max_redirects) = opts.max_redirects {
174            params["maxRedirects"] = json!(max_redirects);
175        }
176        if let Some(max_retries) = opts.max_retries {
177            params["maxRetries"] = json!(max_retries);
178        }
179
180        #[derive(serde::Deserialize)]
181        struct FetchResult {
182            response: ApiResponseData,
183        }
184
185        #[derive(serde::Deserialize)]
186        #[serde(rename_all = "camelCase")]
187        struct ApiResponseData {
188            fetch_uid: String,
189            url: String,
190            status: u16,
191            status_text: String,
192            headers: Vec<HeaderEntry>,
193            #[serde(default)]
194            security_details: Option<crate::protocol::response::SecurityDetails>,
195            #[serde(default)]
196            server_addr: Option<crate::protocol::response::RemoteAddr>,
197        }
198
199        #[derive(serde::Deserialize)]
200        struct HeaderEntry {
201            name: String,
202            value: String,
203        }
204
205        let result: FetchResult = self.base.channel().send("fetch", params).await?;
206
207        let headers: HashMap<String, String> = result
208            .response
209            .headers
210            .into_iter()
211            .map(|h| (h.name, h.value))
212            .collect();
213
214        Ok(APIResponse {
215            context: self.clone(),
216            url: result.response.url,
217            status: result.response.status,
218            status_text: result.response.status_text,
219            headers,
220            fetch_uid: result.response.fetch_uid,
221            security_details: result.response.security_details,
222            server_addr: result.response.server_addr,
223        })
224    }
225
226    /// Disposes this `APIRequestContext`, freeing server resources.
227    ///
228    /// After calling `dispose()`, the context cannot be used for further requests.
229    ///
230    /// See: <https://playwright.dev/docs/api/class-apirequestcontext#api-request-context-dispose>
231    pub async fn dispose(&self) -> Result<()> {
232        self.base
233            .channel()
234            .send_no_result("dispose", json!({}))
235            .await
236    }
237
238    /// Performs an HTTP fetch request and returns the response.
239    ///
240    /// This is the internal method used by `Route::fetch()`. It sends the request
241    /// via the Playwright server and returns the response with headers and body.
242    ///
243    /// # Arguments
244    ///
245    /// * `url` - The URL to fetch
246    /// * `options` - Optional parameters to customize the request
247    ///
248    /// See: <https://playwright.dev/docs/api/class-apirequestcontext#api-request-context-fetch>
249    pub(crate) async fn inner_fetch(
250        &self,
251        url: &str,
252        options: Option<InnerFetchOptions>,
253    ) -> Result<FetchResponse> {
254        let opts = options.unwrap_or_default();
255
256        let mut params = json!({
257            "url": url,
258            "timeout": opts.timeout.unwrap_or(crate::DEFAULT_TIMEOUT_MS)
259        });
260
261        if let Some(method) = opts.method {
262            params["method"] = json!(method);
263        }
264        if let Some(headers) = opts.headers {
265            let headers_array: Vec<Value> = headers
266                .into_iter()
267                .map(|(name, value)| json!({"name": name, "value": value}))
268                .collect();
269            params["headers"] = json!(headers_array);
270        }
271        if let Some(post_data) = opts.post_data {
272            use base64::Engine;
273            let encoded = base64::engine::general_purpose::STANDARD.encode(post_data.as_bytes());
274            params["postData"] = json!(encoded);
275        }
276        if let Some(post_data_bytes) = opts.post_data_bytes {
277            use base64::Engine;
278            let encoded = base64::engine::general_purpose::STANDARD.encode(&post_data_bytes);
279            params["postData"] = json!(encoded);
280        }
281        if let Some(max_redirects) = opts.max_redirects {
282            params["maxRedirects"] = json!(max_redirects);
283        }
284        if let Some(max_retries) = opts.max_retries {
285            params["maxRetries"] = json!(max_retries);
286        }
287
288        // Call the fetch command on APIRequestContext channel
289        #[derive(serde::Deserialize)]
290        struct FetchResult {
291            response: ApiResponseData,
292        }
293
294        #[derive(serde::Deserialize)]
295        #[serde(rename_all = "camelCase")]
296        struct ApiResponseData {
297            fetch_uid: String,
298            #[allow(dead_code)]
299            url: String,
300            status: u16,
301            status_text: String,
302            headers: Vec<HeaderEntry>,
303        }
304
305        #[derive(serde::Deserialize)]
306        struct HeaderEntry {
307            name: String,
308            value: String,
309        }
310
311        let result: FetchResult = self.base.channel().send("fetch", params).await?;
312
313        // Now fetch the response body using fetchResponseBody
314        let body = self.fetch_response_body(&result.response.fetch_uid).await?;
315
316        // Dispose the API response to free server resources
317        let _ = self.dispose_api_response(&result.response.fetch_uid).await;
318
319        Ok(FetchResponse {
320            status: result.response.status,
321            status_text: result.response.status_text,
322            headers: result
323                .response
324                .headers
325                .into_iter()
326                .map(|h| (h.name, h.value))
327                .collect(),
328            body,
329        })
330    }
331
332    /// Fetches the response body for a given fetch UID.
333    async fn fetch_response_body(&self, fetch_uid: &str) -> Result<Vec<u8>> {
334        #[derive(serde::Deserialize)]
335        struct BodyResult {
336            #[serde(default)]
337            binary: Option<String>,
338        }
339
340        let result: BodyResult = self
341            .base
342            .channel()
343            .send("fetchResponseBody", json!({ "fetchUid": fetch_uid }))
344            .await?;
345
346        match result.binary {
347            Some(encoded) if !encoded.is_empty() => {
348                use base64::Engine;
349                base64::engine::general_purpose::STANDARD
350                    .decode(&encoded)
351                    .map_err(|e| {
352                        crate::error::Error::ProtocolError(format!(
353                            "Failed to decode response body: {}",
354                            e
355                        ))
356                    })
357            }
358            _ => Ok(vec![]),
359        }
360    }
361
362    /// Disposes an API response to free server resources.
363    async fn dispose_api_response(&self, fetch_uid: &str) -> Result<()> {
364        self.base
365            .channel()
366            .send_no_result("disposeAPIResponse", json!({ "fetchUid": fetch_uid }))
367            .await
368    }
369}
370
371/// A lazy HTTP response returned by `APIRequestContext` methods.
372///
373/// Unlike [`crate::protocol::route::FetchResponse`] (which eagerly fetches the body),
374/// `APIResponse` holds a `fetch_uid` and fetches the body on demand.
375///
376/// See: <https://playwright.dev/docs/api/class-apiresponse>
377#[derive(Clone)]
378pub struct APIResponse {
379    context: APIRequestContext,
380    url: String,
381    status: u16,
382    status_text: String,
383    headers: HashMap<String, String>,
384    fetch_uid: String,
385    security_details: Option<crate::protocol::response::SecurityDetails>,
386    server_addr: Option<crate::protocol::response::RemoteAddr>,
387}
388
389impl APIResponse {
390    /// Returns the URL of the response.
391    pub fn url(&self) -> &str {
392        &self.url
393    }
394
395    /// Returns the HTTP status code.
396    pub fn status(&self) -> u16 {
397        self.status
398    }
399
400    /// Returns the HTTP status text (e.g., "OK", "Not Found").
401    pub fn status_text(&self) -> &str {
402        &self.status_text
403    }
404
405    /// Returns `true` if the status code is in the 200–299 range.
406    pub fn ok(&self) -> bool {
407        (200..300).contains(&self.status)
408    }
409
410    /// Returns the response headers as a `HashMap<String, String>`.
411    pub fn headers(&self) -> &HashMap<String, String> {
412        &self.headers
413    }
414
415    /// Returns TLS/SSL security details for HTTPS responses, or `None` for
416    /// plain HTTP. Mirrors the browser-side `Response::security_details`.
417    ///
418    /// See: <https://playwright.dev/docs/api/class-apiresponse#api-response-security-details>
419    pub fn security_details(&self) -> Option<&crate::protocol::response::SecurityDetails> {
420        self.security_details.as_ref()
421    }
422
423    /// Returns the server's resolved IP address and port for this response,
424    /// or `None` if unavailable.
425    ///
426    /// See: <https://playwright.dev/docs/api/class-apiresponse#api-response-server-addr>
427    pub fn server_addr(&self) -> Option<&crate::protocol::response::RemoteAddr> {
428        self.server_addr.as_ref()
429    }
430
431    /// Fetches and returns the response body as bytes.
432    ///
433    /// See: <https://playwright.dev/docs/api/class-apiresponse#api-response-body>
434    pub async fn body(&self) -> Result<Vec<u8>> {
435        self.context.fetch_response_body(&self.fetch_uid).await
436    }
437
438    /// Fetches and returns the response body as a UTF-8 string.
439    ///
440    /// See: <https://playwright.dev/docs/api/class-apiresponse#api-response-text>
441    pub async fn text(&self) -> Result<String> {
442        let bytes = self.body().await?;
443        String::from_utf8(bytes).map_err(|e| {
444            crate::error::Error::ProtocolError(format!("Response body is not valid UTF-8: {}", e))
445        })
446    }
447
448    /// Fetches the response body and deserializes it as JSON.
449    ///
450    /// See: <https://playwright.dev/docs/api/class-apiresponse#api-response-json>
451    pub async fn json<T: DeserializeOwned>(&self) -> Result<T> {
452        let bytes = self.body().await?;
453        serde_json::from_slice(&bytes).map_err(|e| {
454            crate::error::Error::ProtocolError(format!("Failed to parse response JSON: {}", e))
455        })
456    }
457
458    /// Disposes this response, freeing server-side resources for the response body.
459    ///
460    /// See: <https://playwright.dev/docs/api/class-apiresponse#api-response-dispose>
461    pub async fn dispose(&self) -> Result<()> {
462        self.context.dispose_api_response(&self.fetch_uid).await
463    }
464}
465
466impl std::fmt::Debug for APIResponse {
467    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
468        f.debug_struct("APIResponse")
469            .field("url", &self.url)
470            .field("status", &self.status)
471            .field("status_text", &self.status_text)
472            .finish()
473    }
474}
475
476/// Options for creating a new `APIRequestContext` via `APIRequest::new_context()`.
477///
478/// See: <https://playwright.dev/docs/api/class-apirequest#api-request-new-context>
479#[derive(Debug, Clone, Default)]
480#[non_exhaustive]
481pub struct APIRequestContextOptions {
482    /// Base URL for all relative requests made with this context.
483    pub base_url: Option<String>,
484    /// Extra HTTP headers to be sent with every request.
485    pub extra_http_headers: Option<HashMap<String, String>>,
486    /// Whether to ignore HTTPS errors when making requests.
487    pub ignore_https_errors: Option<bool>,
488    /// User agent string to send with requests.
489    pub user_agent: Option<String>,
490    /// Default timeout for fetch operations in milliseconds.
491    pub timeout: Option<f64>,
492}
493
494impl APIRequestContextOptions {
495    /// Base URL prepended to relative request paths.
496    pub fn base_url(mut self, base_url: impl Into<String>) -> Self {
497        self.base_url = Some(base_url.into());
498        self
499    }
500    /// Extra HTTP headers sent with every request.
501    pub fn extra_http_headers(mut self, headers: HashMap<String, String>) -> Self {
502        self.extra_http_headers = Some(headers);
503        self
504    }
505    /// Ignore HTTPS certificate errors.
506    pub fn ignore_https_errors(mut self, ignore: bool) -> Self {
507        self.ignore_https_errors = Some(ignore);
508        self
509    }
510    /// User-Agent header value.
511    pub fn user_agent(mut self, user_agent: impl Into<String>) -> Self {
512        self.user_agent = Some(user_agent.into());
513        self
514    }
515    /// Maximum time in milliseconds for each request.
516    pub fn timeout(mut self, timeout: f64) -> Self {
517        self.timeout = Some(timeout);
518        self
519    }
520}
521
522/// Factory for creating standalone `APIRequestContext` instances.
523///
524/// Obtained via `playwright.request()`. Use `new_context()` to create a context
525/// for making HTTP requests outside of a browser page.
526///
527/// `APIRequest` intentionally holds only the channel and connection reference,
528/// NOT a `Playwright` clone. Holding a `Playwright` clone would trigger the
529/// server shutdown Drop impl when the temporary `APIRequest` is dropped.
530///
531/// # Example
532///
533/// ```no_run
534/// use playwright_rs::protocol::Playwright;
535///
536/// #[tokio::main]
537/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
538///     let playwright = Playwright::launch().await?;
539///
540///     let ctx = playwright.request().new_context(None).await?;
541///     let response = ctx.get("https://example.com/api/data", None).await?;
542///     assert!(response.ok());
543///     let body = response.text().await?;
544///
545///     ctx.dispose().await?;
546///     playwright.shutdown().await?;
547///     Ok(())
548/// }
549/// ```
550///
551/// See: <https://playwright.dev/docs/api/class-apirequest>
552pub struct APIRequest {
553    channel: crate::server::channel::Channel,
554    connection: Arc<dyn ConnectionLike>,
555}
556
557impl APIRequest {
558    pub(crate) fn new(
559        channel: crate::server::channel::Channel,
560        connection: Arc<dyn ConnectionLike>,
561    ) -> Self {
562        Self {
563            channel,
564            connection,
565        }
566    }
567
568    /// Creates a new `APIRequestContext` for making HTTP requests.
569    ///
570    /// # Arguments
571    ///
572    /// * `options` — Optional configuration for the new context
573    ///
574    /// See: <https://playwright.dev/docs/api/class-apirequest#api-request-new-context>
575    pub async fn new_context(
576        &self,
577        options: impl Into<Option<APIRequestContextOptions>>,
578    ) -> Result<APIRequestContext> {
579        use crate::server::connection::ConnectionExt;
580
581        let options = options.into();
582        let mut params = json!({});
583
584        if let Some(opts) = options {
585            if let Some(base_url) = opts.base_url {
586                params["baseURL"] = json!(base_url);
587            }
588            if let Some(headers) = opts.extra_http_headers {
589                let arr: Vec<Value> = headers
590                    .into_iter()
591                    .map(|(name, value)| json!({"name": name, "value": value}))
592                    .collect();
593                params["extraHTTPHeaders"] = json!(arr);
594            }
595            if let Some(ignore) = opts.ignore_https_errors {
596                params["ignoreHTTPSErrors"] = json!(ignore);
597            }
598            if let Some(ua) = opts.user_agent {
599                params["userAgent"] = json!(ua);
600            }
601            if let Some(timeout) = opts.timeout {
602                params["timeout"] = json!(timeout);
603            }
604        }
605
606        #[derive(serde::Deserialize)]
607        struct NewRequestResult {
608            request: GuidRef,
609        }
610
611        #[derive(serde::Deserialize)]
612        struct GuidRef {
613            guid: String,
614        }
615
616        let result: NewRequestResult = self.channel.send("newRequest", params).await?;
617
618        self.connection
619            .get_typed::<APIRequestContext>(&result.request.guid)
620            .await
621    }
622}
623
624impl std::fmt::Debug for APIRequest {
625    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
626        f.debug_struct("APIRequest").finish()
627    }
628}
629
630/// Options for APIRequestContext.inner_fetch()
631#[derive(Debug, Clone, Default)]
632pub(crate) struct InnerFetchOptions {
633    pub method: Option<String>,
634    pub headers: Option<std::collections::HashMap<String, String>>,
635    pub post_data: Option<String>,
636    pub post_data_bytes: Option<Vec<u8>>,
637    pub max_redirects: Option<u32>,
638    pub max_retries: Option<u32>,
639    pub timeout: Option<f64>,
640}
641
642impl ChannelOwner for APIRequestContext {
643    fn guid(&self) -> &str {
644        self.base.guid()
645    }
646
647    fn type_name(&self) -> &str {
648        self.base.type_name()
649    }
650
651    fn parent(&self) -> Option<Arc<dyn ChannelOwner>> {
652        self.base.parent()
653    }
654
655    fn connection(&self) -> Arc<dyn ConnectionLike> {
656        self.base.connection()
657    }
658
659    fn initializer(&self) -> &Value {
660        self.base.initializer()
661    }
662
663    fn channel(&self) -> &Channel {
664        self.base.channel()
665    }
666
667    fn dispose(&self, reason: DisposeReason) {
668        self.base.dispose(reason)
669    }
670
671    fn adopt(&self, child: Arc<dyn ChannelOwner>) {
672        self.base.adopt(child)
673    }
674
675    fn add_child(&self, guid: Arc<str>, child: Arc<dyn ChannelOwner>) {
676        self.base.add_child(guid, child)
677    }
678
679    fn remove_child(&self, guid: &str) {
680        self.base.remove_child(guid)
681    }
682
683    fn on_event(&self, method: &str, params: Value) {
684        self.base.on_event(method, params)
685    }
686
687    fn was_collected(&self) -> bool {
688        self.base.was_collected()
689    }
690
691    fn as_any(&self) -> &dyn Any {
692        self
693    }
694}
695
696impl std::fmt::Debug for APIRequestContext {
697    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
698        f.debug_struct("APIRequestContext")
699            .field("guid", &self.guid())
700            .finish()
701    }
702}