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}