spacetimedb_lib/http.rs
1//! `SpacetimeType`-ified HTTP request, response and error types,
2//! for use in the procedure HTTP API.
3//!
4//! The types here are all mirrors of various types within the `http` crate.
5//! That crate's types don't have stable representations or `pub`lic interiors,
6//! so we're forced to define our own representation for the SATS serialization.
7//! These types are that representation.
8//!
9//! Users aren't intended to interact with these types,
10//! Our user-facing APIs should use the `http` crate's types directly, and convert to and from these types internally.
11//!
12//! These types are used in BSATN encoding for interchange between the SpacetimeDB host
13//! and guest WASM modules in the `procedure_http_request` ABI call.
14//! For that reason, the layout of these types must not change.
15//! Because we want, to the extent possible,
16//! to support both (old host, new guest) and (new host, old guest) pairings,
17//! we can't meaningfully make these types extensible, even with tricks like version enum wrappers.
18//! Instead, if/when we want to add new functionality which requires sending additional information,
19//! we'll define a new versioned ABI call which uses new types for interchange.
20
21use spacetimedb_sats::{time_duration::TimeDuration, SpacetimeType};
22
23/// Represents an HTTP request which can be made from a procedure running in a SpacetimeDB database.
24#[derive(Clone, SpacetimeType)]
25#[sats(crate = crate, name = "HttpRequest")]
26pub struct Request {
27 pub method: Method,
28 pub headers: Headers,
29 pub timeout: Option<TimeDuration>,
30 /// A valid URI, sourced from an already-validated `http::Uri`.
31 pub uri: String,
32 pub version: Version,
33}
34
35impl Request {
36 /// Return the size of this request's URI and [`Headers`]
37 /// for purposes of metrics reporting.
38 ///
39 /// Ignores the size of the [`Method`] and [`Version`] as they are effectively constant.
40 ///
41 /// As the body is stored externally to the `Request`, metrics reporting must count its size separately.
42 pub fn size_in_bytes(&self) -> usize {
43 self.uri.len() + self.headers.size_in_bytes()
44 }
45}
46
47/// Represents an HTTP method.
48#[derive(Clone, SpacetimeType, PartialEq, Eq)]
49#[sats(crate = crate, name = "HttpMethod")]
50pub enum Method {
51 Get,
52 Head,
53 Post,
54 Put,
55 Delete,
56 Connect,
57 Options,
58 Trace,
59 Patch,
60 Extension(String),
61}
62
63/// An HTTP version.
64#[derive(Clone, SpacetimeType, PartialEq, Eq)]
65#[sats(crate = crate, name = "HttpVersion")]
66pub enum Version {
67 Http09,
68 Http10,
69 Http11,
70 Http2,
71 Http3,
72}
73
74/// A set of HTTP headers.
75#[derive(Clone, SpacetimeType)]
76#[sats(crate = crate, name = "HttpHeaders")]
77pub struct Headers {
78 // SATS doesn't (and won't) have a multimap type, so just use an array of pairs for the ser/de format.
79 entries: Box<[HttpHeaderPair]>,
80}
81
82// `http::header::IntoIter` only returns the `HeaderName` for the first
83// `HeaderValue` with that name, so we have to manually assign the names.
84struct HeaderIter<I, T> {
85 prev: Option<(Box<str>, T)>,
86 inner: I,
87}
88
89impl<I, T> Iterator for HeaderIter<I, T>
90where
91 I: Iterator<Item = (Option<Box<str>>, T)>,
92{
93 type Item = (Box<str>, T);
94
95 fn next(&mut self) -> Option<Self::Item> {
96 let (prev_k, prev_v) = self
97 .prev
98 .take()
99 .or_else(|| self.inner.next().map(|(k, v)| (k.unwrap(), v)))?;
100 self.prev = self
101 .inner
102 .next()
103 .map(|(next_k, next_v)| (next_k.unwrap_or_else(|| prev_k.clone()), next_v));
104 Some((prev_k, prev_v))
105 }
106
107 fn size_hint(&self) -> (usize, Option<usize>) {
108 self.inner.size_hint()
109 }
110}
111
112impl FromIterator<(Option<Box<str>>, Box<[u8]>)> for Headers {
113 fn from_iter<T: IntoIterator<Item = (Option<Box<str>>, Box<[u8]>)>>(iter: T) -> Self {
114 let inner = iter.into_iter();
115 let entries = HeaderIter { prev: None, inner }
116 .map(|(name, value)| HttpHeaderPair { name, value })
117 .collect();
118 Self { entries }
119 }
120}
121
122impl Headers {
123 #[allow(clippy::should_implement_trait)]
124 pub fn into_iter(self) -> impl Iterator<Item = (Box<str>, Box<[u8]>)> {
125 IntoIterator::into_iter(self.entries).map(|HttpHeaderPair { name, value }| (name, value))
126 }
127
128 /// The sum of the lengths of all the header names and header values.
129 ///
130 /// For headers with multiple values for the same header name,
131 /// the length of the header name is counted once for each occurence.
132 fn size_in_bytes(&self) -> usize {
133 self.entries
134 .iter()
135 .map(|HttpHeaderPair { name, value }| name.len() + value.len())
136 .sum::<usize>()
137 }
138}
139
140#[derive(Clone, SpacetimeType)]
141#[sats(crate = crate, name = "HttpHeaderPair")]
142struct HttpHeaderPair {
143 /// A valid HTTP header name, sourced from an already-validated `http::HeaderName`.
144 name: Box<str>,
145 /// A valid HTTP header value, sourced from an already-validated `http::HeaderValue`.
146 value: Box<[u8]>,
147}
148
149#[derive(Clone, SpacetimeType)]
150#[sats(crate = crate, name = "HttpResponse")]
151pub struct Response {
152 pub headers: Headers,
153 pub version: Version,
154 /// A valid HTTP response status code, sourced from an already-validated `http::StatusCode`.
155 pub code: u16,
156}
157
158impl Response {
159 /// Return the size of this request's [`Headers`] for purposes of metrics reporting.
160 ///
161 /// Ignores the size of the `code` and [`Version`] as they are effectively constant.
162 ///
163 /// As the body is stored externally to the `Response`, metrics reporting must count its size separately.
164 pub fn size_in_bytes(&self) -> usize {
165 self.headers.size_in_bytes()
166 }
167}