use crate::rt::{read_bytes_source_as, read_bytes_source_into};
use crate::IterBuf;
#[cfg(all(feature = "unstable", feature = "rand08"))]
use crate::StdbRng;
#[cfg(feature = "unstable")]
use crate::{try_with_tx, with_tx, Timestamp, TxContext};
use bytes::Bytes;
#[cfg(all(feature = "rand", feature = "unstable"))]
use rand08::RngCore;
#[cfg(feature = "unstable")]
use spacetimedb_lib::db::raw_def::v10::MethodOrAny;
use spacetimedb_lib::http as st_http;
#[cfg(feature = "unstable")]
use spacetimedb_lib::http::{character_is_acceptable_for_route_path, ACCEPTABLE_ROUTE_PATH_CHARS_HUMAN_DESCRIPTION};
#[cfg(feature = "unstable")]
use spacetimedb_lib::Identity;
#[cfg(all(feature = "unstable", feature = "rand"))]
use spacetimedb_lib::Uuid;
use spacetimedb_lib::{bsatn, TimeDuration};
#[cfg(all(feature = "unstable", feature = "rand"))]
use std::cell::Cell;
#[cfg(all(feature = "unstable", feature = "rand08"))]
use std::cell::OnceCell;
#[cfg(feature = "unstable")]
use std::str::FromStr;
pub type Request<T = Body> = http::Request<T>;
pub type Response<T = Body> = http::Response<T>;
#[cfg(feature = "unstable")]
#[doc(inline)]
pub use spacetimedb_bindings_macro::http_handler as handler;
#[cfg(feature = "unstable")]
#[doc(inline)]
pub use spacetimedb_bindings_macro::http_router as router;
#[cfg(feature = "unstable")]
#[non_exhaustive]
pub struct HandlerContext {
pub timestamp: Timestamp,
pub http: HttpClient,
#[cfg(feature = "rand08")]
pub(crate) rng: OnceCell<StdbRng>,
#[cfg(feature = "rand")]
pub(crate) counter_uuid: Cell<u32>,
}
#[cfg(feature = "unstable")]
impl HandlerContext {
pub(crate) fn new(timestamp: Timestamp) -> Self {
Self {
timestamp,
http: HttpClient {},
#[cfg(feature = "rand08")]
rng: OnceCell::new(),
#[cfg(feature = "rand")]
counter_uuid: Cell::new(0),
}
}
pub fn identity(&self) -> Identity {
Identity::from_byte_array(spacetimedb_bindings_sys::identity())
}
pub fn with_tx<T>(&mut self, body: impl Fn(&TxContext) -> T) -> T {
with_tx(body)
}
pub fn try_with_tx<T, E>(&mut self, body: impl Fn(&TxContext) -> Result<T, E>) -> Result<T, E> {
try_with_tx(body)
}
#[cfg(feature = "rand")]
pub fn new_uuid_v4(&self) -> anyhow::Result<Uuid> {
let mut bytes = [0u8; 16];
self.rng().try_fill_bytes(&mut bytes)?;
Ok(Uuid::from_random_bytes_v4(bytes))
}
#[cfg(feature = "rand")]
pub fn new_uuid_v7(&self) -> anyhow::Result<Uuid> {
let mut random_bytes = [0u8; 4];
self.rng().try_fill_bytes(&mut random_bytes)?;
Uuid::from_counter_v7(&self.counter_uuid, self.timestamp, &random_bytes)
}
}
#[cfg(feature = "unstable")]
#[derive(Clone, Copy)]
pub struct Handler {
name: &'static str,
}
#[cfg(feature = "unstable")]
impl Handler {
#[doc(hidden)]
pub const fn new(name: &'static str) -> Self {
Self { name }
}
pub(crate) fn name(&self) -> &'static str {
self.name
}
}
#[cfg(feature = "unstable")]
#[derive(Clone, Default)]
pub struct Router {
routes: Vec<RouteSpec>,
}
#[cfg(feature = "unstable")]
#[derive(Clone)]
pub(crate) struct RouteSpec {
pub method: MethodOrAny,
pub path: String,
pub handler: Handler,
}
#[cfg(feature = "unstable")]
impl Router {
pub fn new() -> Self {
Self::default()
}
pub fn get(self, path: impl Into<String>, handler: Handler) -> Self {
self.add_route(MethodOrAny::Method(st_http::Method::Get), path, handler)
}
pub fn head(self, path: impl Into<String>, handler: Handler) -> Self {
self.add_route(MethodOrAny::Method(st_http::Method::Head), path, handler)
}
pub fn options(self, path: impl Into<String>, handler: Handler) -> Self {
self.add_route(MethodOrAny::Method(st_http::Method::Options), path, handler)
}
pub fn put(self, path: impl Into<String>, handler: Handler) -> Self {
self.add_route(MethodOrAny::Method(st_http::Method::Put), path, handler)
}
pub fn delete(self, path: impl Into<String>, handler: Handler) -> Self {
self.add_route(MethodOrAny::Method(st_http::Method::Delete), path, handler)
}
pub fn post(self, path: impl Into<String>, handler: Handler) -> Self {
self.add_route(MethodOrAny::Method(st_http::Method::Post), path, handler)
}
pub fn patch(self, path: impl Into<String>, handler: Handler) -> Self {
self.add_route(MethodOrAny::Method(st_http::Method::Patch), path, handler)
}
pub fn any(self, path: impl Into<String>, handler: Handler) -> Self {
self.add_route(MethodOrAny::Any, path, handler)
}
pub fn nest(self, path: impl Into<String>, sub_router: Self) -> Self {
let path = path.into();
assert_valid_path(&path);
if self.routes.iter().any(|route| route.path.starts_with(&path)) {
panic!("Cannot nest router at `{path}`; existing routes overlap with nested path");
}
let mut merged = self;
for route in sub_router.routes {
let nested_path = join_paths(&path, &route.path);
merged = merged.add_route(route.method, nested_path, route.handler);
}
merged
}
pub fn merge(self, other_router: Self) -> Self {
let mut merged = self;
for route in other_router.routes {
merged = merged.add_route(route.method, route.path, route.handler);
}
merged
}
pub(crate) fn into_routes(self) -> Vec<RouteSpec> {
self.routes
}
fn add_route(mut self, method: MethodOrAny, path: impl Into<String>, handler: Handler) -> Self {
let path = path.into();
assert_valid_path(&path);
let candidate = RouteSpec {
method: method.clone(),
path: path.clone(),
handler,
};
if self.routes.iter().any(|route| routes_overlap(route, &candidate)) {
panic!("Route conflict for `{path}`");
}
self.routes.push(candidate);
self
}
}
#[cfg(feature = "unstable")]
fn join_paths(prefix: &str, suffix: &str) -> String {
if prefix == "/" {
return suffix.to_string();
}
if suffix == "/" {
return prefix.to_string();
}
let prefix = prefix.trim_end_matches('/');
let suffix = suffix.trim_start_matches('/');
format!("{prefix}/{suffix}")
}
#[cfg(feature = "unstable")]
fn assert_valid_path(path: &str) {
if !path.is_empty() && !path.starts_with('/') {
panic!("Route paths must start with `/`: {path}");
}
if !path.chars().all(character_is_acceptable_for_route_path) {
panic!(
"Route paths may contain only {}: {path}",
ACCEPTABLE_ROUTE_PATH_CHARS_HUMAN_DESCRIPTION
);
}
}
#[cfg(feature = "unstable")]
fn routes_overlap(a: &RouteSpec, b: &RouteSpec) -> bool {
if a.path != b.path {
return false;
}
matches!(a.method, MethodOrAny::Any) || matches!(b.method, MethodOrAny::Any) || a.method == b.method
}
#[non_exhaustive]
pub struct HttpClient {}
impl HttpClient {
pub fn send<B: Into<Body>>(&self, request: http::Request<B>) -> Result<Response, Error> {
let (request, body) = request.map(Into::into).into_parts();
let request = convert_request(request);
let request = bsatn::to_vec(&request).expect("Failed to BSATN-serialize `spacetimedb_lib::http::Request`");
match spacetimedb_bindings_sys::procedure::http_request(&request, &body.into_bytes()) {
Ok((response_source, body_source)) => {
let response = read_bytes_source_as::<st_http::Response>(response_source);
let response = convert_response(response).expect("Invalid http response returned from host");
let body = if body_source == spacetimedb_bindings_sys::raw::BytesSource::INVALID {
Body::from_bytes(Vec::<u8>::new())
} else {
let mut buf = IterBuf::take();
read_bytes_source_into(body_source, &mut buf);
Body::from_bytes(buf.clone())
};
Ok(http::Response::from_parts(response, body))
}
Err(err_source) => {
let message = read_bytes_source_as::<String>(err_source);
Err(Error { message })
}
}
}
pub fn get(&self, uri: impl TryInto<http::Uri, Error: Into<http::Error>>) -> Result<Response, Error> {
self.send(
http::Request::builder()
.method(http::Method::GET)
.uri(uri)
.body(Body::empty())?,
)
}
}
fn convert_request(parts: http::request::Parts) -> st_http::Request {
let http::request::Parts {
method,
uri,
version,
headers,
mut extensions,
..
} = parts;
let timeout = extensions.remove::<Timeout>();
if !extensions.is_empty() {
log::warn!("Converting HTTP `Request` with unrecognized extensions");
}
st_http::Request {
method: match method {
http::Method::GET => st_http::Method::Get,
http::Method::HEAD => st_http::Method::Head,
http::Method::POST => st_http::Method::Post,
http::Method::PUT => st_http::Method::Put,
http::Method::DELETE => st_http::Method::Delete,
http::Method::CONNECT => st_http::Method::Connect,
http::Method::OPTIONS => st_http::Method::Options,
http::Method::TRACE => st_http::Method::Trace,
http::Method::PATCH => st_http::Method::Patch,
_ => st_http::Method::Extension(method.to_string()),
},
headers: headers
.into_iter()
.map(|(k, v)| (k.map(|k| k.as_str().into()), v.as_bytes().into()))
.collect(),
timeout: timeout.map(Into::into),
uri: uri.to_string(),
version: match version {
http::Version::HTTP_09 => st_http::Version::Http09,
http::Version::HTTP_10 => st_http::Version::Http10,
http::Version::HTTP_11 => st_http::Version::Http11,
http::Version::HTTP_2 => st_http::Version::Http2,
http::Version::HTTP_3 => st_http::Version::Http3,
_ => unreachable!("Unknown HTTP version: {version:?}"),
},
}
}
fn convert_response(response: st_http::Response) -> http::Result<http::response::Parts> {
let st_http::Response { headers, version, code } = response;
let (mut response, ()) = http::Response::new(()).into_parts();
response.version = match version {
st_http::Version::Http09 => http::Version::HTTP_09,
st_http::Version::Http10 => http::Version::HTTP_10,
st_http::Version::Http11 => http::Version::HTTP_11,
st_http::Version::Http2 => http::Version::HTTP_2,
st_http::Version::Http3 => http::Version::HTTP_3,
};
response.status = http::StatusCode::from_u16(code)?;
response.headers = headers
.into_iter()
.map(|(k, v)| Ok((k.into_string().try_into()?, v.into_vec().try_into()?)))
.collect::<http::Result<_>>()?;
Ok(response)
}
#[cfg(feature = "unstable")]
pub(crate) fn request_from_wire(request: st_http::Request, body: Bytes) -> http::Request<Body> {
let st_http::Request {
method,
headers,
timeout: _,
uri,
version,
} = request;
let method = match method {
st_http::Method::Get => http::Method::GET,
st_http::Method::Head => http::Method::HEAD,
st_http::Method::Post => http::Method::POST,
st_http::Method::Put => http::Method::PUT,
st_http::Method::Delete => http::Method::DELETE,
st_http::Method::Connect => http::Method::CONNECT,
st_http::Method::Options => http::Method::OPTIONS,
st_http::Method::Trace => http::Method::TRACE,
st_http::Method::Patch => http::Method::PATCH,
st_http::Method::Extension(ext) => {
http::Method::from_bytes(ext.as_bytes()).expect("Invalid HTTP method from host")
}
};
let request = http::Request::builder()
.method(method)
.uri(http::Uri::from_str(&uri).expect("Invalid URI from host"))
.body(Body::from_bytes(body))
.expect("Failed to build request");
let (mut parts, body) = request.into_parts();
parts.version = match version {
st_http::Version::Http09 => http::Version::HTTP_09,
st_http::Version::Http10 => http::Version::HTTP_10,
st_http::Version::Http11 => http::Version::HTTP_11,
st_http::Version::Http2 => http::Version::HTTP_2,
st_http::Version::Http3 => http::Version::HTTP_3,
};
parts.headers = headers
.into_iter()
.map(|(k, v)| {
let name = http::HeaderName::from_bytes(k.as_bytes()).expect("Invalid header name from host");
let value = http::HeaderValue::from_bytes(v.as_ref()).expect("Invalid header value from host");
(name, value)
})
.collect();
http::Request::from_parts(parts, body)
}
#[cfg(feature = "unstable")]
pub(crate) fn response_into_wire(response: http::Response<Body>) -> (st_http::Response, Bytes) {
let (parts, body) = response.into_parts();
let st_response = st_http::Response {
headers: parts
.headers
.into_iter()
.map(|(k, v)| (k.map(|k| k.as_str().into()), v.as_bytes().into()))
.collect(),
version: match parts.version {
http::Version::HTTP_09 => st_http::Version::Http09,
http::Version::HTTP_10 => st_http::Version::Http10,
http::Version::HTTP_11 => st_http::Version::Http11,
http::Version::HTTP_2 => st_http::Version::Http2,
http::Version::HTTP_3 => st_http::Version::Http3,
_ => unreachable!("Unknown HTTP version: {:?}", parts.version),
},
code: parts.status.as_u16(),
};
(st_response, body.into_bytes())
}
pub struct Body {
inner: BodyInner,
}
impl Body {
pub fn into_bytes(self) -> Bytes {
match self.inner {
BodyInner::Bytes(bytes) => bytes,
}
}
pub fn into_string(self) -> Result<String, std::string::FromUtf8Error> {
String::from_utf8(self.into_bytes().into())
}
pub fn into_string_lossy(self) -> String {
self.into_string()
.unwrap_or_else(|e| String::from_utf8_lossy(e.as_bytes()).into_owned())
}
pub fn from_bytes(bytes: impl Into<Bytes>) -> Body {
Body {
inner: BodyInner::Bytes(bytes.into()),
}
}
pub fn empty() -> Body {
().into()
}
pub fn is_empty(&self) -> bool {
match &self.inner {
BodyInner::Bytes(bytes) => bytes.is_empty(),
}
}
}
impl Default for Body {
fn default() -> Self {
Self::empty()
}
}
macro_rules! impl_body_from_bytes {
($bytes:ident : $t:ty => $conv:expr) => {
impl From<$t> for Body {
fn from($bytes: $t) -> Body {
Body::from_bytes($conv)
}
}
};
($t:ty) => {
impl_body_from_bytes!(bytes : $t => bytes);
};
}
impl_body_from_bytes!(String);
impl_body_from_bytes!(Vec<u8>);
impl_body_from_bytes!(Box<[u8]>);
impl_body_from_bytes!(&'static [u8]);
impl_body_from_bytes!(&'static str);
impl_body_from_bytes!(_unit: () => Bytes::new());
enum BodyInner {
Bytes(Bytes),
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub struct Timeout(pub TimeDuration);
impl From<TimeDuration> for Timeout {
fn from(timeout: TimeDuration) -> Timeout {
Timeout(timeout)
}
}
impl From<Timeout> for TimeDuration {
fn from(Timeout(timeout): Timeout) -> TimeDuration {
timeout
}
}
#[derive(Clone, Debug)]
pub struct Error {
message: String,
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let Error { message } = self;
f.write_str(message)
}
}
impl std::error::Error for Error {}
impl From<http::Error> for Error {
fn from(err: http::Error) -> Self {
Error {
message: err.to_string(),
}
}
}
#[cfg(all(test, feature = "unstable"))]
mod tests {
use super::*;
#[test]
fn request_from_wire_preserves_metadata_and_body() {
let request = st_http::Request {
method: st_http::Method::Post,
headers: vec![
(
Some("content-type".into()),
b"application/octet-stream".as_slice().into(),
),
(Some("x-echo".into()), b"value".as_slice().into()),
]
.into_iter()
.collect(),
timeout: None,
uri: "https://example.invalid/upload?x=1".to_string(),
version: st_http::Version::Http2,
};
let request = request_from_wire(request, Bytes::from_static(b"payload"));
assert_eq!(request.method(), http::Method::POST);
assert_eq!(request.version(), http::Version::HTTP_2);
assert_eq!(
request.uri(),
&http::Uri::from_static("https://example.invalid/upload?x=1")
);
assert_eq!(request.headers()["content-type"], "application/octet-stream");
assert_eq!(request.headers()["x-echo"], "value");
assert_eq!(request.into_body().into_bytes(), Bytes::from_static(b"payload"));
}
#[test]
fn response_into_wire_splits_metadata_and_body() {
let response = http::Response::builder()
.status(201)
.version(http::Version::HTTP_11)
.header("content-type", "text/plain")
.header("x-result", "ok")
.body(Body::from_bytes("created"))
.expect("response builder should not fail");
let (response_meta, response_body) = response_into_wire(response);
assert_eq!(response_meta.code, 201);
assert!(matches!(response_meta.version, st_http::Version::Http11));
let headers = response_meta.headers.into_iter().collect::<Vec<_>>();
assert_eq!(headers.len(), 2);
assert_eq!(headers[0].0.as_ref(), "content-type");
assert_eq!(&headers[0].1[..], b"text/plain");
assert_eq!(headers[1].0.as_ref(), "x-result");
assert_eq!(&headers[1].1[..], b"ok");
assert_eq!(response_body, Bytes::from_static(b"created"));
}
}