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