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