seda_sdk_rs/http.rs
1//! HTTP fetch action and associated types for the `seda_runtime_sdk`.
2//!
3//! Defines JSON-serializable request and response structs ([`HttpFetchAction`], [`HttpFetchOptions`], [`HttpFetchResponse`])
4//! and provides [`http_fetch`] for executing HTTP requests via VM FFI calls.
5
6use std::collections::BTreeMap;
7
8use serde::{Deserialize, Serialize};
9
10use crate::errors::Result;
11use crate::{
12 bytes::{Bytes, FromBytes, ToBytes},
13 promise::PromiseStatus,
14};
15
16/// An HTTP fetch action containing the target URL and fetch options.
17/// This action is serialized and sent to the VM for execution.
18#[derive(Serialize, Deserialize, Clone, Debug)]
19pub struct HttpFetchAction {
20 /// The URL to fetch.
21 pub url: String,
22 /// The options for the HTTP fetch request.
23 pub options: HttpFetchOptions,
24}
25
26/// Options for the HTTP fetch request, including method, headers, body, and timeout.
27/// This struct is serialized and sent to the VM for execution.
28#[derive(Serialize, Deserialize, Clone, Debug)]
29pub struct HttpFetchOptions {
30 /// The HTTP method to use for the request.
31 pub method: HttpFetchMethod,
32 /// Headers to include in the request.
33 pub headers: BTreeMap<String, String>,
34 /// The body of the request, if any.
35 pub body: Option<Bytes>,
36 /// Timeout for the request in milliseconds.
37 pub timeout_ms: Option<u32>,
38}
39
40impl Default for HttpFetchOptions {
41 fn default() -> Self {
42 HttpFetchOptions {
43 method: HttpFetchMethod::Get,
44 headers: BTreeMap::new(),
45 body: None,
46 timeout_ms: Some(2_000),
47 }
48 }
49}
50
51/// Represents the HTTP methods that can be used in an HTTP fetch request.
52/// This enum is serialized and sent to the VM for execution.
53/// It represents the various [HTTP methods](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods)
54/// that can be used in an HTTP fetch request.
55#[derive(Serialize, Deserialize, Clone, Debug)]
56#[allow(missing_docs)]
57pub enum HttpFetchMethod {
58 Options,
59 Get,
60 Post,
61 Put,
62 Delete,
63 Head,
64 Trace,
65 Connect,
66 Patch,
67}
68
69impl HttpFetchMethod {
70 /// Returns the string representation, in all caps, of the HTTP method.
71 pub fn as_str(&self) -> &str {
72 match self {
73 HttpFetchMethod::Options => "OPTIONS",
74 HttpFetchMethod::Get => "GET",
75 HttpFetchMethod::Post => "POST",
76 HttpFetchMethod::Put => "PUT",
77 HttpFetchMethod::Delete => "DELETE",
78 HttpFetchMethod::Head => "HEAD",
79 HttpFetchMethod::Trace => "TRACE",
80 HttpFetchMethod::Connect => "CONNECT",
81 HttpFetchMethod::Patch => "PATCH",
82 }
83 }
84}
85
86/// Represents the response from an HTTP fetch request.
87/// This struct is serialized and the result is returned to the caller.
88#[derive(Serialize, Deserialize, Clone, Debug)]
89pub struct HttpFetchResponse {
90 /// HTTP Status code
91 pub status: u16,
92
93 /// Response headers
94 pub headers: BTreeMap<String, String>,
95
96 /// Response body in bytes
97 pub bytes: Vec<u8>,
98
99 /// The final URL that was resolved
100 pub url: String,
101
102 /// The byte length of the response
103 pub content_length: usize,
104}
105
106impl HttpFetchResponse {
107 /// Returns `true` if the status code is in the 2xx range.
108 ///
109 /// # Examples
110 ///
111 /// ```
112 /// use seda_sdk_rs::http::HttpFetchResponse;
113 /// let response = HttpFetchResponse {
114 /// status: 200,
115 /// headers: Default::default(),
116 /// bytes: Vec::new(),
117 /// url: "https://api.example.com/data".to_string(),
118 /// content_length: 0,
119 /// };
120 /// assert!(response.is_ok());
121 /// ```
122 pub fn is_ok(&self) -> bool {
123 self.status >= 200 && self.status <= 299
124 }
125
126 /// Converts a [`PromiseStatus`] into an [`HttpFetchResponse`], treating rejections as errors.
127 ///
128 /// # Errors
129 ///
130 /// Fails if the `PromiseStatus` is not a `Fulfilled` variant or if the deserialization fails.
131 pub fn from_promise(promise_status: PromiseStatus) -> Self {
132 match promise_status {
133 PromiseStatus::Rejected(error) => error.try_into().unwrap(),
134 _ => promise_status.parse().unwrap(),
135 }
136 }
137}
138
139impl ToBytes for HttpFetchResponse {
140 fn to_bytes(self) -> Bytes {
141 serde_json::to_vec(&self).unwrap().to_bytes()
142 }
143}
144
145impl FromBytes for HttpFetchResponse {
146 fn from_bytes(bytes: &[u8]) -> Result<Self> {
147 serde_json::from_slice(bytes).map_err(Into::into)
148 }
149
150 fn from_bytes_vec(bytes: Vec<u8>) -> Result<Self> {
151 serde_json::from_slice(&bytes).map_err(Into::into)
152 }
153}
154
155impl TryFrom<Vec<u8>> for HttpFetchResponse {
156 type Error = serde_json::Error;
157
158 fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
159 serde_json::from_slice(&value)
160 }
161}
162
163/// Performs an HTTP fetch request with the given URL and options.
164/// This wraps the unsafe FFI call to the VM's `http_fetch` function.
165///
166/// # Panics
167///
168/// Panics if the serialization of the [`HttpFetchAction`] fails or if the deserialization of the response fails.
169/// We expect these to never happen in practice, as the SDK is designed to ensure valid inputs.
170///
171/// # Examples
172///
173/// ```no_run
174/// use seda_sdk_rs::{bytes::ToBytes, http::{http_fetch, HttpFetchMethod, HttpFetchOptions}};
175/// use std::collections::BTreeMap;
176///
177/// // Basic GET request
178/// let response = http_fetch("https://api.example.com/data", None);
179/// if response.is_ok() {
180/// println!("Status: {}", response.status);
181/// println!("Body length: {}", response.content_length);
182/// }
183///
184/// // POST request with JSON payload
185/// let mut headers = BTreeMap::new();
186/// headers.insert("Content-Type".to_string(), "application/json".to_string());
187///
188/// let options = HttpFetchOptions {
189/// method: HttpFetchMethod::Post,
190/// headers,
191/// body: Some(serde_json::to_vec(&serde_json::json!({"temperature": 25.5, "unit": "celsius"})).unwrap().to_bytes()),
192/// timeout_ms: Some(5_000),
193/// };
194///
195/// let response = http_fetch("https://weather-api.example.com/update", Some(options));
196///
197/// // Handle the response
198/// if response.is_ok() {
199/// // Access response data
200/// println!("Status code: {}", response.status);
201/// println!("Final URL: {}", response.url);
202/// println!("Response size: {}", response.content_length);
203///
204/// // Process response headers
205/// if let Some(content_type) = response.headers.get("content-type") {
206/// println!("Content-Type: {}", content_type);
207/// }
208///
209/// // Process response body
210/// if !response.bytes.is_empty() {
211/// // Convert bytes to string if it's UTF-8 encoded
212/// if let Ok(body_text) = String::from_utf8(response.bytes.clone()) {
213/// println!("Response body: {}", body_text);
214/// }
215/// }
216/// } else {
217/// println!("Request failed with status: {}", response.status);
218/// }
219/// ```
220pub fn http_fetch<URL: ToString>(url: URL, options: Option<HttpFetchOptions>) -> HttpFetchResponse {
221 let http_action = HttpFetchAction {
222 url: url.to_string(),
223 options: options.unwrap_or_default(),
224 };
225
226 let action = serde_json::to_string(&http_action).unwrap();
227 let result_length = unsafe { super::raw::http_fetch(action.as_ptr(), action.len() as u32) };
228 let mut result_data_ptr = vec![0; result_length as usize];
229
230 unsafe {
231 super::raw::call_result_write(result_data_ptr.as_mut_ptr(), result_length);
232 }
233
234 let promise_status: PromiseStatus =
235 serde_json::from_slice(&result_data_ptr).expect("Could not deserialize http_fetch");
236
237 HttpFetchResponse::from_promise(promise_status)
238}