grafbase_sdk/host_io/
http.rs

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