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