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::FetchResponse;
13use crate::server::channel::Channel;
14use crate::server::channel_owner::{
15    ChannelOwner, ChannelOwnerImpl, DisposeReason, ParentOrConnection,
16};
17use crate::server::connection::ConnectionLike;
18use serde_json::{Value, json};
19use std::any::Any;
20use std::sync::Arc;
21
22/// APIRequestContext provides methods for making HTTP requests.
23///
24/// This is the Playwright protocol object that performs actual HTTP operations.
25/// It is created automatically for each BrowserContext and can be accessed
26/// via `BrowserContext::request()`.
27///
28/// Used internally by `Route::fetch()` to perform the actual network request.
29///
30/// See: <https://playwright.dev/docs/api/class-apirequestcontext>
31#[derive(Clone)]
32pub struct APIRequestContext {
33    base: ChannelOwnerImpl,
34}
35
36impl APIRequestContext {
37    pub fn new(
38        parent: ParentOrConnection,
39        type_name: String,
40        guid: Arc<str>,
41        initializer: Value,
42    ) -> Result<Self> {
43        Ok(Self {
44            base: ChannelOwnerImpl::new(parent, type_name, guid, initializer),
45        })
46    }
47
48    /// Performs an HTTP fetch request and returns the response.
49    ///
50    /// This is the internal method used by `Route::fetch()`. It sends the request
51    /// via the Playwright server and returns the response with headers and body.
52    ///
53    /// # Arguments
54    ///
55    /// * `url` - The URL to fetch
56    /// * `options` - Optional parameters to customize the request
57    ///
58    /// See: <https://playwright.dev/docs/api/class-apirequestcontext#api-request-context-fetch>
59    pub(crate) async fn inner_fetch(
60        &self,
61        url: &str,
62        options: Option<InnerFetchOptions>,
63    ) -> Result<FetchResponse> {
64        let opts = options.unwrap_or_default();
65
66        let mut params = json!({
67            "url": url,
68            "timeout": opts.timeout.unwrap_or(crate::DEFAULT_TIMEOUT_MS)
69        });
70
71        if let Some(method) = opts.method {
72            params["method"] = json!(method);
73        }
74        if let Some(headers) = opts.headers {
75            let headers_array: Vec<Value> = headers
76                .into_iter()
77                .map(|(name, value)| json!({"name": name, "value": value}))
78                .collect();
79            params["headers"] = json!(headers_array);
80        }
81        if let Some(post_data) = opts.post_data {
82            use base64::Engine;
83            let encoded = base64::engine::general_purpose::STANDARD.encode(post_data.as_bytes());
84            params["postData"] = json!(encoded);
85        }
86        if let Some(post_data_bytes) = opts.post_data_bytes {
87            use base64::Engine;
88            let encoded = base64::engine::general_purpose::STANDARD.encode(&post_data_bytes);
89            params["postData"] = json!(encoded);
90        }
91        if let Some(max_redirects) = opts.max_redirects {
92            params["maxRedirects"] = json!(max_redirects);
93        }
94        if let Some(max_retries) = opts.max_retries {
95            params["maxRetries"] = json!(max_retries);
96        }
97
98        // Call the fetch command on APIRequestContext channel
99        #[derive(serde::Deserialize)]
100        struct FetchResult {
101            response: ApiResponseData,
102        }
103
104        #[derive(serde::Deserialize)]
105        #[serde(rename_all = "camelCase")]
106        struct ApiResponseData {
107            fetch_uid: String,
108            #[allow(dead_code)]
109            url: String,
110            status: u16,
111            status_text: String,
112            headers: Vec<HeaderEntry>,
113        }
114
115        #[derive(serde::Deserialize)]
116        struct HeaderEntry {
117            name: String,
118            value: String,
119        }
120
121        let result: FetchResult = self.base.channel().send("fetch", params).await?;
122
123        // Now fetch the response body using fetchResponseBody
124        let body = self.fetch_response_body(&result.response.fetch_uid).await?;
125
126        // Dispose the API response to free server resources
127        let _ = self.dispose_api_response(&result.response.fetch_uid).await;
128
129        Ok(FetchResponse {
130            status: result.response.status,
131            status_text: result.response.status_text,
132            headers: result
133                .response
134                .headers
135                .into_iter()
136                .map(|h| (h.name, h.value))
137                .collect(),
138            body,
139        })
140    }
141
142    /// Fetches the response body for a given fetch UID.
143    async fn fetch_response_body(&self, fetch_uid: &str) -> Result<Vec<u8>> {
144        #[derive(serde::Deserialize)]
145        struct BodyResult {
146            #[serde(default)]
147            binary: Option<String>,
148        }
149
150        let result: BodyResult = self
151            .base
152            .channel()
153            .send("fetchResponseBody", json!({ "fetchUid": fetch_uid }))
154            .await?;
155
156        match result.binary {
157            Some(encoded) if !encoded.is_empty() => {
158                use base64::Engine;
159                base64::engine::general_purpose::STANDARD
160                    .decode(&encoded)
161                    .map_err(|e| {
162                        crate::error::Error::ProtocolError(format!(
163                            "Failed to decode response body: {}",
164                            e
165                        ))
166                    })
167            }
168            _ => Ok(vec![]),
169        }
170    }
171
172    /// Disposes an API response to free server resources.
173    async fn dispose_api_response(&self, fetch_uid: &str) -> Result<()> {
174        self.base
175            .channel()
176            .send_no_result("disposeAPIResponse", json!({ "fetchUid": fetch_uid }))
177            .await
178    }
179}
180
181/// Options for APIRequestContext.inner_fetch()
182#[derive(Debug, Clone, Default)]
183pub(crate) struct InnerFetchOptions {
184    pub method: Option<String>,
185    pub headers: Option<std::collections::HashMap<String, String>>,
186    pub post_data: Option<String>,
187    pub post_data_bytes: Option<Vec<u8>>,
188    pub max_redirects: Option<u32>,
189    pub max_retries: Option<u32>,
190    pub timeout: Option<f64>,
191}
192
193impl ChannelOwner for APIRequestContext {
194    fn guid(&self) -> &str {
195        self.base.guid()
196    }
197
198    fn type_name(&self) -> &str {
199        self.base.type_name()
200    }
201
202    fn parent(&self) -> Option<Arc<dyn ChannelOwner>> {
203        self.base.parent()
204    }
205
206    fn connection(&self) -> Arc<dyn ConnectionLike> {
207        self.base.connection()
208    }
209
210    fn initializer(&self) -> &Value {
211        self.base.initializer()
212    }
213
214    fn channel(&self) -> &Channel {
215        self.base.channel()
216    }
217
218    fn dispose(&self, reason: DisposeReason) {
219        self.base.dispose(reason)
220    }
221
222    fn adopt(&self, child: Arc<dyn ChannelOwner>) {
223        self.base.adopt(child)
224    }
225
226    fn add_child(&self, guid: Arc<str>, child: Arc<dyn ChannelOwner>) {
227        self.base.add_child(guid, child)
228    }
229
230    fn remove_child(&self, guid: &str) {
231        self.base.remove_child(guid)
232    }
233
234    fn on_event(&self, method: &str, params: Value) {
235        self.base.on_event(method, params)
236    }
237
238    fn was_collected(&self) -> bool {
239        self.base.was_collected()
240    }
241
242    fn as_any(&self) -> &dyn Any {
243        self
244    }
245}
246
247impl std::fmt::Debug for APIRequestContext {
248    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
249        f.debug_struct("APIRequestContext")
250            .field("guid", &self.guid())
251            .finish()
252    }
253}