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}