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}