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
102pub enum HttpError {
103 /// The request timed out.
104 Timeout,
105 /// The request could not be built correctly.
106 Request(String),
107 /// The request failed due to an error (server connection failed).
108 Connect(String),
109}
110
111impl From<wit::HttpError> for HttpError {
112 fn from(value: wit::HttpError) -> Self {
113 match value {
114 wit::HttpError::Timeout => Self::Timeout,
115 wit::HttpError::Request(msg) => Self::Request(msg),
116 wit::HttpError::Connect(msg) => Self::Connect(msg),
117 }
118 }
119}
120
121/// A struct that represents an HTTP request.
122#[derive(Debug)]
123pub struct HttpRequest(wit::HttpRequest);
124
125impl HttpRequest {
126 /// Constructs a new `HttpRequestBuilder` for sending a GET request to the specified URL.
127 ///
128 /// # Arguments
129 ///
130 /// * `url` - The URL where the GET request should be sent.
131 ///
132 /// # Returns
133 ///
134 /// A builder object (`HttpRequestBuilder`) that can be used to further customize the HTTP request before execution.
135 pub fn get(url: Url) -> HttpRequestBuilder {
136 Self::builder(url, http::Method::GET)
137 }
138
139 /// Constructs a new `HttpRequestBuilder` for sending a POST request to the specified URL.
140 ///
141 /// # Arguments
142 ///
143 /// * `url` - The URL where the POST 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 post(url: Url) -> HttpRequestBuilder {
149 Self::builder(url, http::Method::POST)
150 }
151
152 /// Constructs a new `HttpRequestBuilder` for sending a PUT request to the specified URL.
153 ///
154 /// # Arguments
155 ///
156 /// * `url` - The URL where the PUT 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 put(url: Url) -> HttpRequestBuilder {
162 Self::builder(url, http::Method::PUT)
163 }
164
165 /// Constructs a new `HttpRequestBuilder` for sending a DELETE request to the specified URL.
166 ///
167 /// # Arguments
168 ///
169 /// * `url` - The URL where the DELETE 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 delete(url: Url) -> HttpRequestBuilder {
175 Self::builder(url, http::Method::DELETE)
176 }
177
178 /// Constructs a new `HttpRequestBuilder` for sending a PATCH request to the specified URL.
179 ///
180 /// # Arguments
181 ///
182 /// * `url` - The URL where the PATCH 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 patch(url: Url) -> HttpRequestBuilder {
188 Self::builder(url, http::Method::PATCH)
189 }
190
191 /// Constructs a new `HttpRequestBuilder` for sending a HEAD request to the specified URL.
192 ///
193 /// # Arguments
194 ///
195 /// * `url` - The URL where the HEAD 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 head(url: Url) -> HttpRequestBuilder {
201 Self::builder(url, http::Method::HEAD)
202 }
203
204 /// Constructs a new `HttpRequestBuilder` for sending an OPTIONS request to the specified URL.
205 ///
206 /// # Arguments
207 ///
208 /// * `url` - The URL where the OPTIONS 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 options(url: Url) -> HttpRequestBuilder {
214 Self::builder(url, http::Method::OPTIONS)
215 }
216
217 /// Constructs a new `HttpRequestBuilder` for sending a TRACE request to the specified URL.
218 ///
219 /// # Arguments
220 ///
221 /// * `url` - The URL where the TRACE 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 trace(url: Url) -> HttpRequestBuilder {
227 Self::builder(url, http::Method::TRACE)
228 }
229
230 /// Constructs a new `HttpRequestBuilder` for sending a CONNECT request to the specified URL.
231 ///
232 /// # Arguments
233 ///
234 /// * `url` - The URL where the CONNECT 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 connect(url: Url) -> HttpRequestBuilder {
240 Self::builder(url, http::Method::CONNECT)
241 }
242
243 /// Constructs a new `HttpRequestBuilder` for sending an HTTP request with the specified method and URL.
244 ///
245 /// # Arguments
246 ///
247 /// * `url` - The URL where the request should be sent.
248 /// * `method` - The HTTP method to use for the request (e.g., GET, POST).
249 ///
250 /// # Returns
251 ///
252 /// A builder object (`HttpRequestBuilder`) that can be used to further customize the HTTP request before execution.
253 pub fn builder(url: Url, method: http::Method) -> HttpRequestBuilder {
254 HttpRequestBuilder {
255 method,
256 url,
257 headers: wit::Headers::new().into(),
258 body: Default::default(),
259 timeout: Default::default(),
260 }
261 }
262}
263
264/// A builder for constructing an `HttpRequest`.
265pub struct HttpRequestBuilder {
266 url: Url,
267 method: http::Method,
268 headers: Headers,
269 body: Vec<u8>,
270 timeout: Option<Duration>,
271}
272
273impl HttpRequestBuilder {
274 /// Mutable access to the URL
275 pub fn url(&mut self) -> &mut url::Url {
276 &mut self.url
277 }
278
279 /// Mutable access to the HTTP headers of the request.
280 pub fn headers(&mut self) -> &mut Headers {
281 &mut self.headers
282 }
283
284 /// Adds a header to the HTTP request.
285 ///
286 /// # Arguments
287 ///
288 /// * `name` - The name of the header.
289 /// * `value` - The value of the header.
290 ///
291 /// This method mutably modifies the builder, allowing headers to be added in sequence.
292 pub fn header(&mut self, name: impl AsHeaderName, value: impl AsHeaderValue) -> &mut Self {
293 self.headers.append(name, value);
294 self
295 }
296
297 /// Sets a timeout for the HTTP request in milliseconds.
298 ///
299 /// # Arguments
300 ///
301 /// * `timeout_ms` - The duration of the timeout in milliseconds.
302 ///
303 /// This method mutably modifies the builder, setting an optional timeout for the request.
304 pub fn timeout(&mut self, timeout: Duration) -> &mut Self {
305 self.timeout = Some(timeout);
306 self
307 }
308
309 /// Sets a JSON 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 JSON format and set as the body of the request.
318 ///
319 /// This method constructs a new `HttpRequest` with a JSON payload, returning it for execution.
320 pub fn json<T: Serialize>(mut self, body: T) -> HttpRequest {
321 self.headers.append("Content-Type", "application/json");
322
323 self.body(serde_json::to_vec(&body).unwrap())
324 }
325
326 /// Sets a form-encoded body for the HTTP request and adds the appropriate `Content-Type` header.
327 ///
328 /// # Type Parameters
329 ///
330 /// * `T` - A type that implements `Serialize`.
331 ///
332 /// # Arguments
333 ///
334 /// * `body` - The data to be serialized into form-urlencoded format and set as the body of the request.
335 ///
336 /// This method constructs a new `HttpRequest` with a URL-encoded payload, returning it for execution.
337 pub fn form<T: Serialize>(mut self, body: T) -> HttpRequest {
338 self.headers.append("Content-Type", "application/x-www-form-urlencoded");
339
340 self.body(serde_urlencoded::to_string(&body).unwrap().into_bytes())
341 }
342
343 /// Sets a raw byte array as the body for the HTTP request.
344 ///
345 /// # Arguments
346 ///
347 /// * `body` - The data to be set as the body of the request in `Vec<u8>` format.
348 ///
349 /// This method constructs and returns a new `HttpRequest` with the specified body.
350 pub fn body(mut self, body: Vec<u8>) -> HttpRequest {
351 self.body = body;
352 self.build()
353 }
354
355 /// Constructs a fully configured `HttpRequest` from the builder.
356 pub fn build(self) -> HttpRequest {
357 HttpRequest(wit::HttpRequest {
358 method: self.method.into(),
359 url: self.url.to_string(),
360 headers: self.headers.into(),
361 body: self.body,
362 timeout_ms: self.timeout.map(|d| d.as_millis() as u64),
363 })
364 }
365}
366
367impl From<HttpRequestBuilder> for HttpRequest {
368 fn from(builder: HttpRequestBuilder) -> Self {
369 builder.build()
370 }
371}
372
373/// A structure representing a batch of HTTP requests.
374pub struct BatchHttpRequest {
375 /// A vector holding individual `crate::wit::HttpRequest` objects that are part of this batch.
376 pub(crate) requests: Vec<wit::HttpRequest>,
377}
378
379impl BatchHttpRequest {
380 /// Constructs a new, empty `BatchHttpRequest`.
381 pub fn new() -> Self {
382 Self { requests: Vec::new() }
383 }
384
385 /// Adds a single HTTP request to the batch.
386 pub fn push(&mut self, request: HttpRequest) {
387 self.requests.push(request.0);
388 }
389
390 /// Returns the number of HTTP requests in the batch.
391 pub fn len(&self) -> usize {
392 self.requests.len()
393 }
394
395 /// Determines whether the batch of HTTP requests is empty.
396 #[must_use]
397 pub fn is_empty(&self) -> bool {
398 self.len() == 0
399 }
400}
401
402impl Default for BatchHttpRequest {
403 fn default() -> Self {
404 Self::new()
405 }
406}
407
408/// A struct that represents an HTTP response.
409pub struct HttpResponse {
410 status_code: http::StatusCode,
411 headers: Headers,
412 body: Vec<u8>,
413}
414
415impl From<wit::HttpResponse> for HttpResponse {
416 fn from(response: wit::HttpResponse) -> Self {
417 Self {
418 status_code: http::StatusCode::from_u16(response.status).expect("Provided by the host"),
419 headers: response.headers.into(),
420 body: response.body,
421 }
422 }
423}
424
425impl HttpResponse {
426 /// Returns the status code of the HTTP response.
427 pub fn status(&self) -> http::StatusCode {
428 self.status_code
429 }
430
431 /// Returns the headers of the HTTP response.
432 pub fn headers(&self) -> &Headers {
433 &self.headers
434 }
435
436 /// Returns the body of the HTTP response.
437 pub fn body(&self) -> &[u8] {
438 &self.body
439 }
440
441 /// Converts the HTTP response body into a `Vec<u8>`.
442 pub fn into_bytes(self) -> Vec<u8> {
443 self.body
444 }
445
446 /// Attempts to convert the HTTP response body into a UTF-8 encoded `String`.
447 ///
448 /// This method takes ownership of the `HttpResponse` and returns a `Result<String, std::string::FromUtf8Error>`.
449 /// It attempts to interpret the bytes in the body as a valid UTF-8 sequence.
450 pub fn text(self) -> Result<String, FromUtf8Error> {
451 String::from_utf8(self.body)
452 }
453
454 /// Attempts to deserialize the HTTP response body as JSON.
455 ///
456 /// This method takes ownership of the `HttpResponse` and returns a `Result<serde_json::Value, serde_json::Error>`.
457 ///
458 /// It attempts to interpret the bytes in the body as valid JSON. The conversion is successful if the
459 /// byte slice represents a valid JSON value according to the JSON specification.
460 pub fn json<'de, T>(&'de self) -> Result<T, JsonDeserializeError>
461 where
462 T: serde::de::Deserialize<'de>,
463 {
464 serde_json::from_slice(&self.body)
465 }
466}