grafbase_sdk/host_io/
http.rs

1//! A module for executing HTTP requests.
2
3use std::{string::FromUtf8Error, time::Duration};
4
5use crate::{types::Headers, wit::HttpMethod};
6pub use http::{HeaderName, HeaderValue, Method, StatusCode};
7pub use serde_json::Error as JsonDeserializeError;
8pub use url::Url;
9
10use crate::{
11    types::{AsHeaderName, AsHeaderValue},
12    wit::{self, HttpClient},
13};
14use serde::Serialize;
15
16/// Executes a single HTTP request and returns a result containing either an `HttpResponse` or an `HttpError`.
17///
18/// This function delegates the execution of the HTTP request to the underlying `HttpClient`, which handles
19/// the asynchronous sending of the request in the host runtime. From the perspective of the guest, this operation is blocking.
20/// While awaiting the response, other tasks can be executed concurrently by the host thread.
21///
22/// # Arguments
23///
24/// * `request` - A reference to an `HttpRequest` that encapsulates the HTTP method, URL, headers,
25///   body, and optional timeout settings for the request to be sent.
26///
27/// # Returns
28///
29/// This function returns a `Result<HttpResponse, HttpError>`, which represents either the successful response from the server
30/// (`HttpResponse`) or an error that occurred during the execution of the HTTP request (`HttpError`).
31pub fn execute(request: impl Into<HttpRequest>) -> Result<HttpResponse, HttpError> {
32    let request: HttpRequest = request.into();
33    HttpClient::execute(request.0).map(Into::into).map_err(Into::into)
34}
35
36/// Executes multiple HTTP requests in a batch and returns their results.
37///
38/// This function takes advantage of `HttpClient::execute_many` to handle multiple requests concurrently
39/// within the host runtime environment. Similar to executing single requests, this operation is blocking from
40/// the guest's point of view but non-blocking on the host side where tasks can run asynchronously.
41///
42/// # Arguments
43///
44/// * `requests` - A `BatchHttpRequest` containing a vector of individual `crate::wit::HttpRequest`
45///   objects. Each represents a complete HTTP request with its own settings and payload data to be sent.
46///
47/// # Returns
48///
49/// It returns a `Vec<Result<HttpResponse, HttpError>>`, which is a vector where each element corresponds
50/// to the result of executing one of the batched requests. Each element will either contain an `HttpResponse`
51/// if the request was successful or an `HttpError` if there was an issue with that particular request.
52pub fn execute_many(requests: BatchHttpRequest) -> Vec<Result<HttpResponse, HttpError>> {
53    HttpClient::execute_many(requests.requests)
54        .into_iter()
55        .map(|r| r.map(Into::into).map_err(Into::into))
56        .collect()
57}
58
59impl From<http::Method> for HttpMethod {
60    fn from(value: http::Method) -> Self {
61        if value == http::Method::GET {
62            Self::Get
63        } else if value == http::Method::POST {
64            Self::Post
65        } else if value == http::Method::PUT {
66            Self::Put
67        } else if value == http::Method::DELETE {
68            Self::Delete
69        } else if value == http::Method::HEAD {
70            Self::Head
71        } else if value == http::Method::OPTIONS {
72            Self::Options
73        } else if value == http::Method::CONNECT {
74            Self::Connect
75        } else if value == http::Method::TRACE {
76            Self::Trace
77        } else if value == http::Method::PATCH {
78            Self::Patch
79        } else {
80            unreachable!()
81        }
82    }
83}
84
85impl From<HttpMethod> for http::Method {
86    fn from(value: HttpMethod) -> Self {
87        match value {
88            HttpMethod::Get => http::Method::GET,
89            HttpMethod::Post => http::Method::POST,
90            HttpMethod::Put => http::Method::PUT,
91            HttpMethod::Delete => http::Method::DELETE,
92            HttpMethod::Patch => http::Method::PATCH,
93            HttpMethod::Head => http::Method::HEAD,
94            HttpMethod::Options => http::Method::OPTIONS,
95            HttpMethod::Connect => http::Method::CONNECT,
96            HttpMethod::Trace => http::Method::TRACE,
97        }
98    }
99}
100
101/// HTTP error
102#[derive(Clone, Debug)]
103pub enum HttpError {
104    /// The request timed out.
105    Timeout,
106    /// The request could not be built correctly.
107    Request(String),
108    /// The request failed due to an error (server connection failed).
109    Connect(String),
110}
111
112impl std::error::Error for HttpError {}
113
114impl std::fmt::Display for HttpError {
115    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116        match self {
117            Self::Timeout => write!(f, "The request timed out"),
118            Self::Request(msg) => write!(f, "The request could not be built correctly: {}", msg),
119            Self::Connect(msg) => write!(f, "The request failed due to an error: {}", msg),
120        }
121    }
122}
123
124impl From<wit::HttpError> for HttpError {
125    fn from(value: wit::HttpError) -> Self {
126        match value {
127            wit::HttpError::Timeout => Self::Timeout,
128            wit::HttpError::Request(msg) => Self::Request(msg),
129            wit::HttpError::Connect(msg) => Self::Connect(msg),
130        }
131    }
132}
133
134/// A struct that represents an HTTP request.
135#[derive(Debug)]
136pub struct HttpRequest(wit::HttpRequest);
137
138impl HttpRequest {
139    /// Constructs a new `HttpRequestBuilder` for sending a GET request to the specified URL.
140    ///
141    /// # Arguments
142    ///
143    /// * `url` - The URL where the GET request should be sent.
144    ///
145    /// # Returns
146    ///
147    /// A builder object (`HttpRequestBuilder`) that can be used to further customize the HTTP request before execution.
148    pub fn get(url: Url) -> HttpRequestBuilder {
149        Self::builder(url, http::Method::GET)
150    }
151
152    /// Constructs a new `HttpRequestBuilder` for sending a POST request to the specified URL.
153    ///
154    /// # Arguments
155    ///
156    /// * `url` - The URL where the POST request should be sent.
157    ///
158    /// # Returns
159    ///
160    /// A builder object (`HttpRequestBuilder`) that can be used to further customize the HTTP request before execution.
161    pub fn post(url: Url) -> HttpRequestBuilder {
162        Self::builder(url, http::Method::POST)
163    }
164
165    /// Constructs a new `HttpRequestBuilder` for sending a PUT request to the specified URL.
166    ///
167    /// # Arguments
168    ///
169    /// * `url` - The URL where the PUT request should be sent.
170    ///
171    /// # Returns
172    ///
173    /// A builder object (`HttpRequestBuilder`) that can be used to further customize the HTTP request before execution.
174    pub fn put(url: Url) -> HttpRequestBuilder {
175        Self::builder(url, http::Method::PUT)
176    }
177
178    /// Constructs a new `HttpRequestBuilder` for sending a DELETE request to the specified URL.
179    ///
180    /// # Arguments
181    ///
182    /// * `url` - The URL where the DELETE request should be sent.
183    ///
184    /// # Returns
185    ///
186    /// A builder object (`HttpRequestBuilder`) that can be used to further customize the HTTP request before execution.
187    pub fn delete(url: Url) -> HttpRequestBuilder {
188        Self::builder(url, http::Method::DELETE)
189    }
190
191    /// Constructs a new `HttpRequestBuilder` for sending a PATCH request to the specified URL.
192    ///
193    /// # Arguments
194    ///
195    /// * `url` - The URL where the PATCH request should be sent.
196    ///
197    /// # Returns
198    ///
199    /// A builder object (`HttpRequestBuilder`) that can be used to further customize the HTTP request before execution.
200    pub fn patch(url: Url) -> HttpRequestBuilder {
201        Self::builder(url, http::Method::PATCH)
202    }
203
204    /// Constructs a new `HttpRequestBuilder` for sending a HEAD request to the specified URL.
205    ///
206    /// # Arguments
207    ///
208    /// * `url` - The URL where the HEAD request should be sent.
209    ///
210    /// # Returns
211    ///
212    /// A builder object (`HttpRequestBuilder`) that can be used to further customize the HTTP request before execution.
213    pub fn head(url: Url) -> HttpRequestBuilder {
214        Self::builder(url, http::Method::HEAD)
215    }
216
217    /// Constructs a new `HttpRequestBuilder` for sending an OPTIONS request to the specified URL.
218    ///
219    /// # Arguments
220    ///
221    /// * `url` - The URL where the OPTIONS request should be sent.
222    ///
223    /// # Returns
224    ///
225    /// A builder object (`HttpRequestBuilder`) that can be used to further customize the HTTP request before execution.
226    pub fn options(url: Url) -> HttpRequestBuilder {
227        Self::builder(url, http::Method::OPTIONS)
228    }
229
230    /// Constructs a new `HttpRequestBuilder` for sending a TRACE request to the specified URL.
231    ///
232    /// # Arguments
233    ///
234    /// * `url` - The URL where the TRACE request should be sent.
235    ///
236    /// # Returns
237    ///
238    /// A builder object (`HttpRequestBuilder`) that can be used to further customize the HTTP request before execution.
239    pub fn trace(url: Url) -> HttpRequestBuilder {
240        Self::builder(url, http::Method::TRACE)
241    }
242
243    /// Constructs a new `HttpRequestBuilder` for sending a CONNECT request to the specified URL.
244    ///
245    /// # Arguments
246    ///
247    /// * `url` - The URL where the CONNECT request should be sent.
248    ///
249    /// # Returns
250    ///
251    /// A builder object (`HttpRequestBuilder`) that can be used to further customize the HTTP request before execution.
252    pub fn connect(url: Url) -> HttpRequestBuilder {
253        Self::builder(url, http::Method::CONNECT)
254    }
255
256    /// Constructs a new `HttpRequestBuilder` for sending an HTTP request with the specified method and URL.
257    ///
258    /// # Arguments
259    ///
260    /// * `url` - The URL where the request should be sent.
261    /// * `method` - The HTTP method to use for the request (e.g., GET, POST).
262    ///
263    /// # Returns
264    ///
265    /// A builder object (`HttpRequestBuilder`) that can be used to further customize the HTTP request before execution.
266    pub fn builder(url: Url, method: http::Method) -> HttpRequestBuilder {
267        HttpRequestBuilder {
268            method,
269            url,
270            headers: wit::Headers::new().into(),
271            body: Default::default(),
272            timeout: Default::default(),
273        }
274    }
275}
276
277/// A builder for constructing an `HttpRequest`.
278pub struct HttpRequestBuilder {
279    url: Url,
280    method: http::Method,
281    headers: Headers,
282    body: Vec<u8>,
283    timeout: Option<Duration>,
284}
285
286impl HttpRequestBuilder {
287    /// Mutable access to the URL
288    pub fn url(&mut self) -> &mut url::Url {
289        &mut self.url
290    }
291
292    /// Mutable access to the HTTP headers of the request.
293    pub fn headers(&mut self) -> &mut Headers {
294        &mut self.headers
295    }
296
297    /// Adds a header to the HTTP request.
298    ///
299    /// # Arguments
300    ///
301    /// * `name` - The name of the header.
302    /// * `value` - The value of the header.
303    ///
304    /// This method mutably modifies the builder, allowing headers to be added in sequence.
305    pub fn header(&mut self, name: impl AsHeaderName, value: impl AsHeaderValue) -> &mut Self {
306        self.headers.append(name, value);
307        self
308    }
309
310    /// Sets a timeout for the HTTP request in milliseconds.
311    ///
312    /// # Arguments
313    ///
314    /// * `timeout_ms` - The duration of the timeout in milliseconds.
315    ///
316    /// This method mutably modifies the builder, setting an optional timeout for the request.
317    pub fn timeout(&mut self, timeout: Duration) -> &mut Self {
318        self.timeout = Some(timeout);
319        self
320    }
321
322    /// Sets a JSON body for the HTTP request and adds the appropriate `Content-Type` header.
323    ///
324    /// # Type Parameters
325    ///
326    /// * `T` - A type that implements `Serialize`.
327    ///
328    /// # Arguments
329    ///
330    /// * `body` - The data to be serialized into JSON format and set as the body of the request.
331    ///
332    /// This method constructs a new `HttpRequest` with a JSON payload, returning it for execution.
333    pub fn json<T: Serialize>(mut self, body: T) -> HttpRequest {
334        self.headers.append("Content-Type", "application/json");
335
336        self.body(serde_json::to_vec(&body).unwrap())
337    }
338
339    /// Sets a form-encoded body for the HTTP request and adds the appropriate `Content-Type` header.
340    ///
341    /// # Type Parameters
342    ///
343    /// * `T` - A type that implements `Serialize`.
344    ///
345    /// # Arguments
346    ///
347    /// * `body` - The data to be serialized into form-urlencoded format and set as the body of the request.
348    ///
349    /// This method constructs a new `HttpRequest` with a URL-encoded payload, returning it for execution.
350    pub fn form<T: Serialize>(mut self, body: T) -> HttpRequest {
351        self.headers.append("Content-Type", "application/x-www-form-urlencoded");
352
353        self.body(serde_urlencoded::to_string(&body).unwrap().into_bytes())
354    }
355
356    /// Sets a raw byte array as the body for the HTTP request.
357    ///
358    /// # Arguments
359    ///
360    /// * `body` - The data to be set as the body of the request in `Vec<u8>` format.
361    ///
362    /// This method constructs and returns a new `HttpRequest` with the specified body.
363    pub fn body(mut self, body: Vec<u8>) -> HttpRequest {
364        self.body = body;
365        self.build()
366    }
367
368    /// Constructs a fully configured `HttpRequest` from the builder.
369    pub fn build(self) -> HttpRequest {
370        HttpRequest(wit::HttpRequest {
371            method: self.method.into(),
372            url: self.url.to_string(),
373            headers: self.headers.into(),
374            body: self.body,
375            timeout_ms: self.timeout.map(|d| d.as_millis() as u64),
376        })
377    }
378}
379
380impl From<HttpRequestBuilder> for HttpRequest {
381    fn from(builder: HttpRequestBuilder) -> Self {
382        builder.build()
383    }
384}
385
386/// A structure representing a batch of HTTP requests.
387pub struct BatchHttpRequest {
388    /// A vector holding individual `crate::wit::HttpRequest` objects that are part of this batch.
389    pub(crate) requests: Vec<wit::HttpRequest>,
390}
391
392impl BatchHttpRequest {
393    /// Constructs a new, empty `BatchHttpRequest`.
394    pub fn new() -> Self {
395        Self { requests: Vec::new() }
396    }
397
398    /// Adds a single HTTP request to the batch.
399    pub fn push(&mut self, request: HttpRequest) {
400        self.requests.push(request.0);
401    }
402
403    /// Returns the number of HTTP requests in the batch.
404    pub fn len(&self) -> usize {
405        self.requests.len()
406    }
407
408    /// Determines whether the batch of HTTP requests is empty.
409    #[must_use]
410    pub fn is_empty(&self) -> bool {
411        self.len() == 0
412    }
413}
414
415impl Default for BatchHttpRequest {
416    fn default() -> Self {
417        Self::new()
418    }
419}
420
421/// A struct that represents an HTTP response.
422pub struct HttpResponse {
423    status_code: http::StatusCode,
424    headers: Headers,
425    body: Vec<u8>,
426}
427
428impl From<wit::HttpResponse> for HttpResponse {
429    fn from(response: wit::HttpResponse) -> Self {
430        Self {
431            status_code: http::StatusCode::from_u16(response.status).expect("Provided by the host"),
432            headers: response.headers.into(),
433            body: response.body,
434        }
435    }
436}
437
438impl HttpResponse {
439    /// Returns the status code of the HTTP response.
440    pub fn status(&self) -> http::StatusCode {
441        self.status_code
442    }
443
444    /// Returns the headers of the HTTP response.
445    pub fn headers(&self) -> &Headers {
446        &self.headers
447    }
448
449    /// Returns the body of the HTTP response.
450    pub fn body(&self) -> &[u8] {
451        &self.body
452    }
453
454    /// Converts the HTTP response body into a `Vec<u8>`.
455    pub fn into_bytes(self) -> Vec<u8> {
456        self.body
457    }
458
459    /// Attempts to convert the HTTP response body into a UTF-8 encoded `String`.
460    ///
461    /// This method takes ownership of the `HttpResponse` and returns a `Result<String, std::string::FromUtf8Error>`.
462    /// It attempts to interpret the bytes in the body as a valid UTF-8 sequence.
463    pub fn text(self) -> Result<String, FromUtf8Error> {
464        String::from_utf8(self.body)
465    }
466
467    /// Attempts to deserialize the HTTP response body as JSON.
468    ///
469    /// This method takes ownership of the `HttpResponse` and returns a `Result<serde_json::Value, serde_json::Error>`.
470    ///
471    /// It attempts to interpret the bytes in the body as valid JSON. The conversion is successful if the
472    /// byte slice represents a valid JSON value according to the JSON specification.
473    pub fn json<'de, T>(&'de self) -> Result<T, JsonDeserializeError>
474    where
475        T: serde::de::Deserialize<'de>,
476    {
477        serde_json::from_slice(&self.body)
478    }
479}