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)]
421pub struct APIRequestContextOptions {
422    /// Base URL for all relative requests made with this context.
423    pub base_url: Option<String>,
424    /// Extra HTTP headers to be sent with every request.
425    pub extra_http_headers: Option<HashMap<String, String>>,
426    /// Whether to ignore HTTPS errors when making requests.
427    pub ignore_https_errors: Option<bool>,
428    /// User agent string to send with requests.
429    pub user_agent: Option<String>,
430    /// Default timeout for fetch operations in milliseconds.
431    pub timeout: Option<f64>,
432}
433
434/// Factory for creating standalone `APIRequestContext` instances.
435///
436/// Obtained via `playwright.request()`. Use `new_context()` to create a context
437/// for making HTTP requests outside of a browser page.
438///
439/// `APIRequest` intentionally holds only the channel and connection reference,
440/// NOT a `Playwright` clone. Holding a `Playwright` clone would trigger the
441/// server shutdown Drop impl when the temporary `APIRequest` is dropped.
442///
443/// # Example
444///
445/// ```ignore
446/// use playwright_rs::protocol::Playwright;
447///
448/// #[tokio::main]
449/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
450///     let playwright = Playwright::launch().await?;
451///
452///     let ctx = playwright.request().new_context(None).await?;
453///     let response = ctx.get("https://example.com/api/data", None).await?;
454///     assert!(response.ok());
455///     let body = response.text().await?;
456///
457///     ctx.dispose().await?;
458///     playwright.shutdown().await?;
459///     Ok(())
460/// }
461/// ```
462///
463/// See: <https://playwright.dev/docs/api/class-apirequest>
464pub struct APIRequest {
465    channel: crate::server::channel::Channel,
466    connection: Arc<dyn ConnectionLike>,
467}
468
469impl APIRequest {
470    pub(crate) fn new(
471        channel: crate::server::channel::Channel,
472        connection: Arc<dyn ConnectionLike>,
473    ) -> Self {
474        Self {
475            channel,
476            connection,
477        }
478    }
479
480    /// Creates a new `APIRequestContext` for making HTTP requests.
481    ///
482    /// # Arguments
483    ///
484    /// * `options` — Optional configuration for the new context
485    ///
486    /// See: <https://playwright.dev/docs/api/class-apirequest#api-request-new-context>
487    pub async fn new_context(
488        &self,
489        options: Option<APIRequestContextOptions>,
490    ) -> Result<APIRequestContext> {
491        use crate::server::connection::ConnectionExt;
492
493        let mut params = json!({});
494
495        if let Some(opts) = options {
496            if let Some(base_url) = opts.base_url {
497                params["baseURL"] = json!(base_url);
498            }
499            if let Some(headers) = opts.extra_http_headers {
500                let arr: Vec<Value> = headers
501                    .into_iter()
502                    .map(|(name, value)| json!({"name": name, "value": value}))
503                    .collect();
504                params["extraHTTPHeaders"] = json!(arr);
505            }
506            if let Some(ignore) = opts.ignore_https_errors {
507                params["ignoreHTTPSErrors"] = json!(ignore);
508            }
509            if let Some(ua) = opts.user_agent {
510                params["userAgent"] = json!(ua);
511            }
512            if let Some(timeout) = opts.timeout {
513                params["timeout"] = json!(timeout);
514            }
515        }
516
517        #[derive(serde::Deserialize)]
518        struct NewRequestResult {
519            request: GuidRef,
520        }
521
522        #[derive(serde::Deserialize)]
523        struct GuidRef {
524            guid: String,
525        }
526
527        let result: NewRequestResult = self.channel.send("newRequest", params).await?;
528
529        self.connection
530            .get_typed::<APIRequestContext>(&result.request.guid)
531            .await
532    }
533}
534
535impl std::fmt::Debug for APIRequest {
536    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
537        f.debug_struct("APIRequest").finish()
538    }
539}
540
541/// Options for APIRequestContext.inner_fetch()
542#[derive(Debug, Clone, Default)]
543pub(crate) struct InnerFetchOptions {
544    pub method: Option<String>,
545    pub headers: Option<std::collections::HashMap<String, String>>,
546    pub post_data: Option<String>,
547    pub post_data_bytes: Option<Vec<u8>>,
548    pub max_redirects: Option<u32>,
549    pub max_retries: Option<u32>,
550    pub timeout: Option<f64>,
551}
552
553impl ChannelOwner for APIRequestContext {
554    fn guid(&self) -> &str {
555        self.base.guid()
556    }
557
558    fn type_name(&self) -> &str {
559        self.base.type_name()
560    }
561
562    fn parent(&self) -> Option<Arc<dyn ChannelOwner>> {
563        self.base.parent()
564    }
565
566    fn connection(&self) -> Arc<dyn ConnectionLike> {
567        self.base.connection()
568    }
569
570    fn initializer(&self) -> &Value {
571        self.base.initializer()
572    }
573
574    fn channel(&self) -> &Channel {
575        self.base.channel()
576    }
577
578    fn dispose(&self, reason: DisposeReason) {
579        self.base.dispose(reason)
580    }
581
582    fn adopt(&self, child: Arc<dyn ChannelOwner>) {
583        self.base.adopt(child)
584    }
585
586    fn add_child(&self, guid: Arc<str>, child: Arc<dyn ChannelOwner>) {
587        self.base.add_child(guid, child)
588    }
589
590    fn remove_child(&self, guid: &str) {
591        self.base.remove_child(guid)
592    }
593
594    fn on_event(&self, method: &str, params: Value) {
595        self.base.on_event(method, params)
596    }
597
598    fn was_collected(&self) -> bool {
599        self.base.was_collected()
600    }
601
602    fn as_any(&self) -> &dyn Any {
603        self
604    }
605}
606
607impl std::fmt::Debug for APIRequestContext {
608    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
609        f.debug_struct("APIRequestContext")
610            .field("guid", &self.guid())
611            .finish()
612    }
613}