Skip to main content

trailbase_wasm/
http.rs

1use futures_util::future::LocalBoxFuture;
2use serde::de::DeserializeOwned;
3use trailbase_wasm_common::HttpContext;
4use wstd::http::server::{Finished, Responder};
5use wstd::io::{Cursor, Empty, empty};
6
7pub use http::{HeaderMap, HeaderValue, Method, StatusCode, Version, header};
8pub use trailbase_wasm_common::HttpContextUser as User;
9
10pub type Response<T = BoundedBody<Vec<u8>>> = http::Response<T>;
11
12#[derive(Clone, Debug)]
13pub struct HttpError {
14  pub status: StatusCode,
15  pub message: Option<String>,
16}
17
18impl HttpError {
19  pub fn status(status: StatusCode) -> Self {
20    return Self {
21      status,
22      message: None,
23    };
24  }
25
26  pub fn message(status: StatusCode, message: impl std::string::ToString) -> Self {
27    return Self {
28      status,
29      message: Some(message.to_string()),
30    };
31  }
32}
33
34impl From<HttpError> for Response {
35  fn from(value: HttpError) -> Self {
36    return value.into_response();
37  }
38}
39
40type HttpHandler = Box<
41  dyn FnOnce(
42    HttpContext,
43    http::Request<wstd::http::body::IncomingBody>,
44    wstd::http::server::Responder,
45  ) -> LocalBoxFuture<'static, Finished>,
46>;
47
48pub struct HttpRoute {
49  pub method: Method,
50  pub path: String,
51  pub handler: HttpHandler,
52}
53
54impl HttpRoute {
55  pub fn new<F, R, B>(method: Method, path: impl std::string::ToString, f: F) -> Self
56  where
57    // NOTE: Send + Sync aren't strictly needed. We could also accept AsyncFnOnce, however let's
58    // start more constraint and see where it takes us.
59    F: (AsyncFn(Request) -> R) + Send + Sync + 'static,
60    R: IntoResponse<B>,
61    B: wstd::http::body::Body,
62  {
63    return Self {
64      method,
65      path: path.to_string(),
66      handler: Box::new(
67        move |context: HttpContext,
68              req: http::Request<wstd::http::body::IncomingBody>,
69              responder: Responder| {
70          let (head, body) = req.into_parts();
71          let Ok(url) = to_url(head.uri) else {
72            return Box::pin(responder.respond(empty_error_response(StatusCode::BAD_REQUEST)));
73          };
74
75          let req = Request {
76            head: Parts {
77              method: head.method,
78              uri: url,
79              version: head.version,
80              headers: head.headers,
81              user: context.user,
82              path_params: context.path_params,
83            },
84            body,
85          };
86
87          return Box::pin(async move {
88            #[allow(clippy::let_and_return)]
89            let response = responder.respond(f(req).await.into_response()).await;
90
91            // TODO: Poll tasks.
92
93            response
94          });
95        },
96      ),
97    };
98  }
99}
100
101pub mod routing {
102  use super::{HttpRoute, IntoResponse, Method, Request};
103
104  pub fn get<F, R, B>(path: impl std::string::ToString, f: F) -> HttpRoute
105  where
106    F: (AsyncFn(Request) -> R) + Send + Sync + 'static,
107    R: IntoResponse<B>,
108    B: wstd::http::body::Body,
109  {
110    return HttpRoute::new(Method::GET, path, f);
111  }
112
113  pub fn post<F, R, B>(path: impl std::string::ToString, f: F) -> HttpRoute
114  where
115    F: (AsyncFn(Request) -> R) + Send + Sync + 'static,
116    R: IntoResponse<B>,
117    B: wstd::http::body::Body,
118  {
119    return HttpRoute::new(Method::POST, path, f);
120  }
121
122  pub fn patch<F, R, B>(path: impl std::string::ToString, f: F) -> HttpRoute
123  where
124    F: (AsyncFn(Request) -> R) + Send + Sync + 'static,
125    R: IntoResponse<B>,
126    B: wstd::http::body::Body,
127  {
128    return HttpRoute::new(Method::PATCH, path, f);
129  }
130
131  pub fn delete<F, R, B>(path: impl std::string::ToString, f: F) -> HttpRoute
132  where
133    F: (AsyncFn(Request) -> R) + Send + Sync + 'static,
134    R: IntoResponse<B>,
135    B: wstd::http::body::Body,
136  {
137    return HttpRoute::new(Method::DELETE, path, f);
138  }
139}
140
141// Disallow external construction.
142#[non_exhaustive]
143#[derive(Clone, Debug)]
144pub struct Parts {
145  /// The request's method
146  pub method: Method,
147
148  /// The request's URI
149  pub uri: url::Url,
150
151  /// The request's version
152  pub version: Version,
153
154  /// The request's headers
155  pub headers: HeaderMap<HeaderValue>,
156
157  /// User metadata
158  pub user: Option<User>,
159
160  /// Path params, e.g. /test/{param}/.
161  pub path_params: Vec<(String, String)>,
162}
163
164#[derive(Debug)]
165pub struct Request {
166  head: Parts,
167  body: wstd::http::body::IncomingBody,
168}
169
170impl Request {
171  #[inline]
172  pub fn body(&mut self) -> &mut wstd::http::body::IncomingBody {
173    return &mut self.body;
174  }
175
176  #[inline]
177  pub fn url(&self) -> &url::Url {
178    return &self.head.uri;
179  }
180
181  pub fn query_parse<T: DeserializeOwned>(&self) -> Result<T, HttpError> {
182    let query = self.head.uri.query().unwrap_or_default();
183    let deserializer =
184      serde_urlencoded::Deserializer::new(url::form_urlencoded::parse(query.as_bytes()));
185    return serde_path_to_error::deserialize(deserializer)
186      .map_err(|err| HttpError::message(StatusCode::BAD_REQUEST, err));
187  }
188
189  // pub fn query_pairs(&self) -> url::form_urlencoded::Parse<'_> {
190  //   self.head.uri.query_pairs()
191  // }
192
193  pub fn query_param(&self, param: &str) -> Option<String> {
194    return self
195      .head
196      .uri
197      .query_pairs()
198      .find(|(p, _v)| p == param)
199      .map(|(_p, v)| v.to_string());
200  }
201
202  pub fn path_param(&self, param: &str) -> Option<&str> {
203    return self
204      .head
205      .path_params
206      .iter()
207      .find(|(p, _v)| p == param)
208      .map(|(_p, v)| v.as_str());
209  }
210
211  #[inline]
212  pub fn method(&self) -> &Method {
213    return &self.head.method;
214  }
215
216  #[inline]
217  pub fn version(&self) -> &Version {
218    return &self.head.version;
219  }
220
221  #[inline]
222  pub fn header(&self, key: &str) -> Option<&HeaderValue> {
223    return self.head.headers.get(key);
224  }
225
226  #[inline]
227  pub fn user(&self) -> Option<&User> {
228    return self.head.user.as_ref();
229  }
230}
231
232fn to_url(uri: http::Uri) -> Result<url::Url, url::ParseError> {
233  let http::uri::Parts {
234    scheme,
235    authority,
236    path_and_query,
237    ..
238  } = uri.into_parts();
239
240  return match (scheme, authority, path_and_query) {
241    (Some(s), Some(a), Some(p)) => url::Url::parse(&format!("{s}://{a}/{p}")),
242    (_, _, Some(p)) => url::Url::parse(p.as_str()),
243    _ => Err(url::ParseError::RelativeUrlWithCannotBeABaseBase),
244  };
245}
246
247/// An HTTP body with a known length
248#[derive(Debug, Default)]
249pub struct BoundedBody<T>(Cursor<T>);
250
251impl<T: AsRef<[u8]>> wstd::io::AsyncRead for BoundedBody<T> {
252  async fn read(&mut self, buf: &mut [u8]) -> wstd::io::Result<usize> {
253    self.0.read(buf).await
254  }
255}
256
257impl<T: AsRef<[u8]>> wstd::http::body::Body for BoundedBody<T> {
258  fn len(&self) -> Option<usize> {
259    Some(self.0.get_ref().as_ref().len())
260  }
261}
262
263/// Conversion into a `Body`.
264///
265/// NOTE: We have our own trait over wstd::http::body::IntoBody to avoid possible future conflicts
266/// when implementing IntoResponse for Result<B: IntoBody, HttpError>.
267pub trait IntoBody {
268  /// What type of `Body` are we turning this into?
269  type IntoBody: wstd::http::body::Body;
270  /// Convert into `Body`.
271  fn into_body(self) -> Self::IntoBody;
272}
273
274impl IntoBody for () {
275  type IntoBody = wstd::io::Empty;
276
277  fn into_body(self) -> Self::IntoBody {
278    return wstd::io::empty();
279  }
280}
281
282impl IntoBody for String {
283  type IntoBody = BoundedBody<Vec<u8>>;
284  fn into_body(self) -> Self::IntoBody {
285    BoundedBody(Cursor::new(self.into_bytes()))
286  }
287}
288
289impl IntoBody for &str {
290  type IntoBody = BoundedBody<Vec<u8>>;
291  fn into_body(self) -> Self::IntoBody {
292    BoundedBody(Cursor::new(self.to_owned().into_bytes()))
293  }
294}
295
296impl IntoBody for Vec<u8> {
297  type IntoBody = BoundedBody<Vec<u8>>;
298  fn into_body(self) -> Self::IntoBody {
299    BoundedBody(Cursor::new(self))
300  }
301}
302
303impl IntoBody for &[u8] {
304  type IntoBody = BoundedBody<Vec<u8>>;
305  fn into_body(self) -> Self::IntoBody {
306    BoundedBody(Cursor::new(self.to_owned()))
307  }
308}
309
310pub trait IntoResponse<B> {
311  fn into_response(self) -> http::Response<B>;
312}
313
314impl<B: wstd::http::body::Body> IntoResponse<B> for Response<B> {
315  fn into_response(self) -> http::Response<B> {
316    return self;
317  }
318}
319
320impl<B: wstd::http::body::Body, Err: IntoResponse<B>> IntoResponse<B> for Result<Response<B>, Err> {
321  fn into_response(self) -> http::Response<B> {
322    return match self {
323      Ok(resp) => resp,
324      Err(err) => err.into_response(),
325    };
326  }
327}
328
329impl<B: IntoBody> IntoResponse<B::IntoBody> for B {
330  fn into_response(self) -> http::Response<B::IntoBody> {
331    return http::Response::new(self.into_body());
332  }
333}
334
335impl<B: IntoBody<IntoBody = BoundedBody<Vec<u8>>>> IntoResponse<BoundedBody<Vec<u8>>>
336  for Result<B, HttpError>
337{
338  fn into_response(self) -> http::Response<BoundedBody<Vec<u8>>> {
339    return match self {
340      Ok(body) => http::Response::new(body.into_body()),
341      Err(err) => build_response(err.status, err.message.unwrap_or_default().into_body()),
342    };
343  }
344}
345
346impl IntoResponse<BoundedBody<Vec<u8>>> for HttpError {
347  fn into_response(self) -> http::Response<BoundedBody<Vec<u8>>> {
348    return build_response(self.status, self.message.unwrap_or_default().into_body());
349  }
350}
351
352impl IntoResponse<BoundedBody<Vec<u8>>> for Result<(), HttpError> {
353  fn into_response(self) -> http::Response<BoundedBody<Vec<u8>>> {
354    return match self {
355      Ok(_) => http::Response::new("".into_body()),
356      Err(err) => err.into_response(),
357    };
358  }
359}
360
361#[derive(Debug, Clone, Copy, Default)]
362#[must_use]
363pub struct Json<T>(pub T);
364
365impl<T> IntoResponse<BoundedBody<Vec<u8>>> for Json<T>
366where
367  T: serde::Serialize,
368{
369  fn into_response(self) -> http::Response<BoundedBody<Vec<u8>>> {
370    return build_json_response(StatusCode::OK, self.0);
371  }
372}
373
374impl<T> IntoResponse<BoundedBody<Vec<u8>>> for std::result::Result<Json<T>, HttpError>
375where
376  T: serde::Serialize,
377{
378  fn into_response(self) -> http::Response<BoundedBody<Vec<u8>>> {
379    return match self {
380      Ok(json) => {
381        return build_json_response(StatusCode::OK, json.0);
382      }
383      Err(err) => build_response(err.status, err.message.unwrap_or_default().into_body()),
384    };
385  }
386}
387
388/// An HTML response.
389///
390/// Will automatically get `Content-Type: text/html`.
391#[derive(Clone, Copy, Debug)]
392#[must_use]
393pub struct Html<T>(pub T);
394
395impl<T> IntoResponse<BoundedBody<Vec<u8>>> for Html<T>
396where
397  T: IntoResponse<BoundedBody<Vec<u8>>>,
398{
399  fn into_response(self) -> Response {
400    let mut r = self.0.into_response();
401    r.headers_mut().insert(
402      http::header::CONTENT_TYPE,
403      http::HeaderValue::from_static("text/html; charset=utf-8"),
404    );
405    return r;
406  }
407}
408
409#[derive(Debug, Clone)]
410#[must_use = "needs to be returned from a handler or otherwise turned into a Response to be useful"]
411pub struct Redirect {
412  status_code: StatusCode,
413  location: http::header::HeaderValue,
414}
415
416impl Redirect {
417  pub fn to(uri: &str) -> Self {
418    Self::with_status_code(StatusCode::SEE_OTHER, uri)
419  }
420
421  pub fn temporary(uri: &str) -> Self {
422    Self::with_status_code(StatusCode::TEMPORARY_REDIRECT, uri)
423  }
424
425  pub fn permanent(uri: &str) -> Self {
426    Self::with_status_code(StatusCode::PERMANENT_REDIRECT, uri)
427  }
428
429  fn with_status_code(status_code: StatusCode, uri: &str) -> Self {
430    assert!(
431      status_code.is_redirection(),
432      "not a redirection status code"
433    );
434
435    Self {
436      status_code,
437      location: HeaderValue::try_from(uri).expect("URI isn't a valid header value"),
438    }
439  }
440}
441
442impl<B: wstd::http::body::Body + Default> IntoResponse<B> for Redirect {
443  fn into_response(self) -> http::Response<B> {
444    let mut response = http::Response::<B>::default();
445    *response.status_mut() = self.status_code;
446    response
447      .headers_mut()
448      .insert(http::header::LOCATION, self.location);
449    return response;
450  }
451}
452
453pub(crate) fn empty_error_response(status: StatusCode) -> http::Response<Empty> {
454  let mut response = http::Response::new(empty());
455  *response.status_mut() = status;
456  return response;
457}
458
459fn internal_error_response() -> http::Response<BoundedBody<Vec<u8>>> {
460  return build_response(StatusCode::INTERNAL_SERVER_ERROR, "".into_body());
461}
462
463#[inline]
464fn build_response(
465  status: StatusCode,
466  body: BoundedBody<Vec<u8>>,
467) -> http::Response<BoundedBody<Vec<u8>>> {
468  let mut response = http::Response::new(body);
469  *response.status_mut() = status;
470  return response;
471}
472
473#[inline]
474fn build_json_response<T: serde::Serialize>(
475  status: StatusCode,
476  value: T,
477) -> http::Response<BoundedBody<Vec<u8>>> {
478  let Ok(bytes) = serde_json::to_vec(&value) else {
479    return internal_error_response();
480  };
481
482  let mut response = build_response(status, bytes.into_body());
483  response.headers_mut().insert(
484    http::header::CONTENT_TYPE,
485    HeaderValue::from_static("application/json"),
486  );
487
488  return response;
489}