wasm_runner_sdk/
http.rs

1//! HTTP client for making outbound requests.
2//!
3//! This module provides a high-level API for making HTTP requests from
4//! within a WASM module. It wraps the async host operations in a
5//! synchronous, ergonomic interface.
6
7use crate::abi;
8use serde::de::DeserializeOwned;
9use serde::Serialize;
10use std::time::Duration;
11
12/// Error type for HTTP client operations.
13#[derive(Debug, Clone)]
14pub enum HttpError {
15    /// The request failed to start.
16    RequestFailed(String),
17    /// The request timed out.
18    Timeout,
19    /// The operation ID was invalid.
20    InvalidOperation,
21    /// Failed to parse the response.
22    ParseError(String),
23    /// Network or transport error.
24    NetworkError(String),
25}
26
27impl std::fmt::Display for HttpError {
28    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29        match self {
30            HttpError::RequestFailed(msg) => write!(f, "Request failed: {}", msg),
31            HttpError::Timeout => write!(f, "Request timed out"),
32            HttpError::InvalidOperation => write!(f, "Invalid operation ID"),
33            HttpError::ParseError(msg) => write!(f, "Parse error: {}", msg),
34            HttpError::NetworkError(msg) => write!(f, "Network error: {}", msg),
35        }
36    }
37}
38
39impl std::error::Error for HttpError {}
40
41/// Response from an HTTP request.
42#[derive(Debug, Clone)]
43pub struct HttpResponse {
44    /// HTTP status code.
45    pub status: u16,
46    /// Response body as bytes.
47    pub body: Vec<u8>,
48}
49
50impl HttpResponse {
51    /// Returns true if the status code indicates success (2xx).
52    pub fn is_success(&self) -> bool {
53        self.status >= 200 && self.status < 300
54    }
55
56    /// Returns the body as a UTF-8 string.
57    pub fn text(&self) -> Result<String, std::string::FromUtf8Error> {
58        String::from_utf8(self.body.clone())
59    }
60
61    /// Parses the body as JSON.
62    pub fn json<T: DeserializeOwned>(&self) -> Result<T, serde_json::Error> {
63        serde_json::from_slice(&self.body)
64    }
65}
66
67/// Builder for HTTP requests.
68#[derive(Debug, Clone)]
69pub struct RequestBuilder {
70    method: String,
71    url: String,
72    headers: Vec<(String, String)>,
73    body: Vec<u8>,
74    timeout_ms: i32,
75}
76
77impl RequestBuilder {
78    /// Creates a new request builder.
79    fn new(method: impl Into<String>, url: impl Into<String>) -> Self {
80        Self {
81            method: method.into(),
82            url: url.into(),
83            headers: Vec::new(),
84            body: Vec::new(),
85            timeout_ms: 30000, // 30 second default
86        }
87    }
88
89    /// Adds a header to the request.
90    pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
91        self.headers.push((name.into(), value.into()));
92        self
93    }
94
95    /// Sets the Content-Type header.
96    pub fn content_type(self, content_type: impl Into<String>) -> Self {
97        self.header("content-type", content_type)
98    }
99
100    /// Sets the Authorization header with a Bearer token.
101    pub fn bearer_auth(self, token: impl Into<String>) -> Self {
102        self.header("authorization", format!("Bearer {}", token.into()))
103    }
104
105    /// Sets the request body as bytes.
106    pub fn body(mut self, body: impl Into<Vec<u8>>) -> Self {
107        self.body = body.into();
108        self
109    }
110
111    /// Sets the request body as a string.
112    pub fn text(self, text: impl Into<String>) -> Self {
113        self.content_type("text/plain")
114            .body(text.into().into_bytes())
115    }
116
117    /// Sets the request body as JSON.
118    pub fn json<T: Serialize>(self, value: &T) -> Result<Self, serde_json::Error> {
119        let body = serde_json::to_vec(value)?;
120        Ok(self.content_type("application/json").body(body))
121    }
122
123    /// Sets the request timeout.
124    pub fn timeout(mut self, duration: Duration) -> Self {
125        self.timeout_ms = duration.as_millis().min(i32::MAX as u128) as i32;
126        self
127    }
128
129    /// Sends the request and waits for the response.
130    pub fn send(self) -> Result<HttpResponse, HttpError> {
131        // Serialize headers
132        let headers_bytes = abi::serialize_pairs(&self.headers);
133
134        // Start the request
135        let op_id = unsafe {
136            abi::http_fetch_with_options(
137                self.method.as_ptr(),
138                self.method.len() as i32,
139                self.url.as_ptr(),
140                self.url.len() as i32,
141                headers_bytes.as_ptr(),
142                headers_bytes.len() as i32,
143                self.body.as_ptr(),
144                self.body.len() as i32,
145            )
146        };
147
148        if op_id < 0 {
149            return Err(HttpError::RequestFailed("Failed to start HTTP request".into()));
150        }
151
152        // Poll for completion
153        let poll_result = unsafe { abi::operation_poll(op_id, self.timeout_ms) };
154
155        match poll_result {
156            1 => {
157                // Success - get the result
158                let status = unsafe { abi::operation_result_status(op_id) };
159
160                // Get body size first
161                let body_size = unsafe { abi::operation_result_body(op_id, std::ptr::null_mut(), 0) };
162
163                let body = if body_size > 0 {
164                    let mut body_buf = vec![0u8; body_size as usize];
165                    unsafe {
166                        abi::operation_result_body(
167                            op_id,
168                            body_buf.as_mut_ptr(),
169                            body_buf.len() as i32,
170                        );
171                    }
172                    body_buf
173                } else {
174                    Vec::new()
175                };
176
177                // Free the operation
178                unsafe { abi::operation_free(op_id) };
179
180                Ok(HttpResponse {
181                    status: status as u16,
182                    body,
183                })
184            }
185            0 => {
186                // Timeout
187                unsafe { abi::operation_free(op_id) };
188                Err(HttpError::Timeout)
189            }
190            -1 => {
191                // Error - try to get error message from body
192                let body_size = unsafe { abi::operation_result_body(op_id, std::ptr::null_mut(), 0) };
193                let error_msg = if body_size > 0 {
194                    let mut buf = vec![0u8; body_size as usize];
195                    unsafe {
196                        abi::operation_result_body(op_id, buf.as_mut_ptr(), buf.len() as i32);
197                    }
198                    String::from_utf8_lossy(&buf).into_owned()
199                } else {
200                    "Unknown error".into()
201                };
202
203                unsafe { abi::operation_free(op_id) };
204                Err(HttpError::NetworkError(error_msg))
205            }
206            -2 => {
207                Err(HttpError::InvalidOperation)
208            }
209            _ => {
210                unsafe { abi::operation_free(op_id) };
211                Err(HttpError::NetworkError(format!("Unexpected poll result: {}", poll_result)))
212            }
213        }
214    }
215}
216
217/// HTTP client for making outbound requests.
218///
219/// # Example
220/// ```ignore
221/// use wasm_runner_sdk::http::Client;
222///
223/// let client = Client::new();
224/// let response = client.get("https://api.example.com/data").send()?;
225/// let data: MyData = response.json()?;
226/// ```
227#[derive(Debug, Clone, Default)]
228pub struct Client {
229    default_headers: Vec<(String, String)>,
230}
231
232impl Client {
233    /// Creates a new HTTP client.
234    pub fn new() -> Self {
235        Self::default()
236    }
237
238    /// Creates a client with default headers that will be sent with every request.
239    pub fn with_default_headers(headers: Vec<(String, String)>) -> Self {
240        Self {
241            default_headers: headers,
242        }
243    }
244
245    /// Adds a default header that will be sent with every request.
246    pub fn default_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
247        self.default_headers.push((name.into(), value.into()));
248        self
249    }
250
251    /// Creates a GET request.
252    pub fn get(&self, url: impl Into<String>) -> RequestBuilder {
253        self.request("GET", url)
254    }
255
256    /// Creates a POST request.
257    pub fn post(&self, url: impl Into<String>) -> RequestBuilder {
258        self.request("POST", url)
259    }
260
261    /// Creates a PUT request.
262    pub fn put(&self, url: impl Into<String>) -> RequestBuilder {
263        self.request("PUT", url)
264    }
265
266    /// Creates a DELETE request.
267    pub fn delete(&self, url: impl Into<String>) -> RequestBuilder {
268        self.request("DELETE", url)
269    }
270
271    /// Creates a PATCH request.
272    pub fn patch(&self, url: impl Into<String>) -> RequestBuilder {
273        self.request("PATCH", url)
274    }
275
276    /// Creates a HEAD request.
277    pub fn head(&self, url: impl Into<String>) -> RequestBuilder {
278        self.request("HEAD", url)
279    }
280
281    /// Creates a request with the given method.
282    pub fn request(&self, method: impl Into<String>, url: impl Into<String>) -> RequestBuilder {
283        let mut builder = RequestBuilder::new(method, url);
284        for (name, value) in &self.default_headers {
285            builder = builder.header(name.clone(), value.clone());
286        }
287        builder
288    }
289}
290
291/// Convenience function for simple GET requests.
292pub fn get(url: impl Into<String>) -> Result<HttpResponse, HttpError> {
293    Client::new().get(url).send()
294}
295
296/// Convenience function for simple POST requests with JSON body.
297pub fn post_json<T: Serialize>(url: impl Into<String>, body: &T) -> Result<HttpResponse, HttpError> {
298    Client::new()
299        .post(url)
300        .json(body)
301        .map_err(|e| HttpError::ParseError(e.to_string()))?
302        .send()
303}