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}