grafbase_sdk/host_io/
http.rs

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