diff --git a/Cargo.lock b/Cargo.lock index 8b0daef28c3..91feabfc9d0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7716,7 +7716,6 @@ dependencies = [ "enum-as-inner", "enum-map", "hex", - "http 1.3.1", "insta", "itertools 0.12.1", "log", diff --git a/crates/bindings/src/http.rs b/crates/bindings/src/http.rs index 4f4ef4c1982..68870192cfe 100644 --- a/crates/bindings/src/http.rs +++ b/crates/bindings/src/http.rs @@ -6,14 +6,16 @@ //! while [`send`](HttpClient::send) allows more complex requests with headers, bodies and other methods. use bytes::Bytes; -pub use http::{Request, Response}; -pub use spacetimedb_lib::http::{Error, Timeout}; use crate::{ rt::{read_bytes_source_as, read_bytes_source_into}, IterBuf, }; -use spacetimedb_lib::{bsatn, http as st_http}; +use spacetimedb_lib::{bsatn, http as st_http, TimeDuration}; + +pub type Request = http::Request; + +pub type Response = http::Response; /// Allows performing HTTP requests via [`HttpClient::send`] and [`HttpClient::get`]. /// @@ -43,8 +45,7 @@ impl HttpClient { /// and a timeout of 100 milliseconds, then treat the response as a string and log it: /// /// ```norun - /// # use spacetimedb::{procedure, ProcedureContext}; - /// # use spacetimedb::http::{Request, Timeout}; + /// # use spacetimedb::{procedure, ProcedureContext, http::Timeout}; /// # use std::time::Duration; /// # #[procedure] /// # fn post_somewhere(ctx: &mut ProcedureContext) { @@ -73,16 +74,15 @@ impl HttpClient { /// # } /// /// ``` - pub fn send>(&self, request: Request) -> Result, Error> { + pub fn send>(&self, request: http::Request) -> Result { let (request, body) = request.map(Into::into).into_parts(); - let request = st_http::Request::from(request); + 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::(response_source); - let response = - http::response::Parts::try_from(response).expect("Invalid http response returned from host"); + let response = convert_response(response).expect("Invalid http response returned from host"); let mut buf = IterBuf::take(); read_bytes_source_into(body_source, &mut buf); let body = Body::from_bytes(buf.clone()); @@ -90,8 +90,8 @@ impl HttpClient { Ok(http::Response::from_parts(response, body)) } Err(err_source) => { - let error = read_bytes_source_as::(err_source); - Err(error) + let message = read_bytes_source_as::(err_source); + Err(Error { message }) } } } @@ -121,17 +121,79 @@ impl HttpClient { /// } /// # } /// ``` - pub fn get(&self, uri: impl TryInto>) -> Result, Error> { + pub fn get(&self, uri: impl TryInto>) -> Result { self.send( http::Request::builder() - .method("GET") + .method(http::Method::GET) .uri(uri) - .body(Body::empty()) - .map_err(|err| Error::from_display(&err))?, + .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::(); + 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 { + 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::>()?; + Ok(response) +} + /// Represents the body of an HTTP request or response. pub struct Body { inner: BodyInner, @@ -208,3 +270,58 @@ impl_body_from_bytes!(_unit: () => Bytes::new()); enum BodyInner { Bytes(Bytes), } + +/// An HTTP extension to specify a timeout for requests made by a procedure running in a SpacetimeDB database. +/// +/// Pass an instance of this type to [`http::request::Builder::extension`] to set a timeout on a request. +/// +/// This timeout applies to the entire request, +/// from when the headers are first sent to when the response body is fully downloaded. +/// This is sometimes called a total timeout, the sum of the connect timeout and the read timeout. +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct Timeout(pub TimeDuration); + +impl From for Timeout { + fn from(timeout: TimeDuration) -> Timeout { + Timeout(timeout) + } +} + +impl From for TimeDuration { + fn from(Timeout(timeout): Timeout) -> TimeDuration { + timeout + } +} + +/// An error that may arise from an HTTP call. +#[derive(Clone, Debug)] +pub struct Error { + /// A string message describing the error. + /// + /// It would be nice if we could store a more interesting object here, + /// ideally a type-erased `dyn Trait` cause, + /// rather than just a string, similar to how `anyhow` does. + /// This is not possible because we need to serialize `Error` for transport to WASM, + /// meaning it must have a concrete static type. + /// `reqwest::Error`, which is the source for these, + /// is type-erased enough that the best we can do (at least, the best we can do easily) + /// is to eagerly string-ify the 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 for Error { + fn from(err: http::Error) -> Self { + Error { + message: err.to_string(), + } + } +} diff --git a/crates/bindings/tests/snapshots/deps__spacetimedb_bindings_dependencies.snap b/crates/bindings/tests/snapshots/deps__spacetimedb_bindings_dependencies.snap index 8d3943d8bda..a71b95c50fe 100644 --- a/crates/bindings/tests/snapshots/deps__spacetimedb_bindings_dependencies.snap +++ b/crates/bindings/tests/snapshots/deps__spacetimedb_bindings_dependencies.snap @@ -83,7 +83,6 @@ spacetimedb │ ├── derive_more (*) │ ├── enum_as_inner (*) │ ├── hex -│ ├── http (*) │ ├── itertools (*) │ ├── log │ ├── spacetimedb_bindings_macro (*) diff --git a/crates/core/src/error.rs b/crates/core/src/error.rs index 6a41beaebf1..31e3c79ceeb 100644 --- a/crates/core/src/error.rs +++ b/crates/core/src/error.rs @@ -282,7 +282,7 @@ pub enum NodesError { #[error("Failed to scheduled timer: {0}")] ScheduleError(#[source] ScheduleError), #[error("HTTP request failed: {0}")] - HttpError(#[from] spacetimedb_lib::http::Error), + HttpError(String), } impl From for NodesError { diff --git a/crates/core/src/host/instance_env.rs b/crates/core/src/host/instance_env.rs index aeff5127bb6..4bb73c65913 100644 --- a/crates/core/src/host/instance_env.rs +++ b/crates/core/src/host/instance_env.rs @@ -618,68 +618,45 @@ impl InstanceEnv { // TODO(procedure-metrics): record size in bytes of request. + fn http_error(err: E) -> NodesError { + NodesError::HttpError(err.to_string()) + } + // Then convert the request into an `http::Request`, a semi-standard "lingua franca" type in the Rust ecosystem, // and map its body into a type `reqwest` will like. - fn convert_request(request: st_http::Request, body: bytes::Bytes) -> Result { - let mut request: http::request::Parts = - request.try_into().map_err(|err| st_http::Error::from_display(&err))?; - - // Pull our timeout extension, if any, out of the `http::Request` extensions. - // reqwest has its own timeout extension, which is where we'll provide this. - let timeout = request.extensions.remove::(); + let (request, timeout) = convert_http_request(request).map_err(http_error)?; - let request = http::Request::from_parts(request, body.to_vec()); + let request = http::Request::from_parts(request, body); - let mut reqwest: reqwest::Request = request.try_into().map_err(|err| st_http::Error::from_display(&err))?; + let mut reqwest: reqwest::Request = request.try_into().map_err(http_error)?; - // If the user requested a timeout using our extension, slot it in to reqwest's timeout. - // Clamp to the range `0..HTTP_DEFAULT_TIMEOUT`. - let timeout = timeout - .map(|timeout| timeout.timeout.to_duration().unwrap_or(Duration::ZERO)) - .unwrap_or(HTTP_DEFAULT_TIMEOUT) - .min(HTTP_DEFAULT_TIMEOUT); + // If the user requested a timeout using our extension, slot it in to reqwest's timeout. + // Clamp to the range `0..HTTP_DEFAULT_TIMEOUT`. + let timeout = timeout.unwrap_or(HTTP_DEFAULT_TIMEOUT).min(HTTP_DEFAULT_TIMEOUT); - // reqwest's timeout covers from the start of the request to the end of reading the body, - // so there's no need to do our own timeout operation. - *reqwest.timeout_mut() = Some(timeout); - - Ok(reqwest) - } + // reqwest's timeout covers from the start of the request to the end of reading the body, + // so there's no need to do our own timeout operation. + *reqwest.timeout_mut() = Some(timeout); - // If for whatever reason reqwest doesn't like our `http::Request`, - // surface that error to the guest so customers can debug and provide a more appropriate request. - let reqwest = convert_request(request, body)?; + let reqwest = reqwest; // TODO(procedure-metrics): record size in bytes of response, time spent awaiting response. // Actually execute the HTTP request! - // We'll wrap this future in a `tokio::time::timeout` before `await`ing it. - let get_response_and_download_body = async { - // TODO(perf): Stash a long-lived `Client` in the env somewhere, rather than building a new one for each call. - let response = reqwest::Client::new() - .execute(reqwest) - .await - .map_err(|err| st_http::Error::from_display(&err))?; - - // Download the response body, which in all likelihood will be a stream, - // as reqwest seems to prefer that. - // Note that this will be wrapped in the same `tokio::time::timeout` as the above `execute` call. - let (parts, body) = http::Response::from(response).into_parts(); - let body = http_body_util::BodyExt::collect(body) - .await - .map_err(|err| st_http::Error::from_display(&err))?; - - // Map the collected body into our `spacetimedb_lib::http::Body` type, - // then wrap it back in an `http::Response`. - Ok::<_, st_http::Error>((parts, body.to_bytes())) - }; + // TODO(perf): Stash a long-lived `Client` in the env somewhere, rather than building a new one for each call. + let response = reqwest::Client::new().execute(reqwest).await.map_err(http_error)?; - // If the request failed, surface that error to the guest so customer logic can handle it. - let (response, body) = get_response_and_download_body.await?; + // Download the response body, which in all likelihood will be a stream, + // as reqwest seems to prefer that. + let (response, body) = http::Response::from(response).into_parts(); + let body = http_body_util::BodyExt::collect(body) + .await + .map_err(http_error)? + .to_bytes(); // Transform the `http::Response` into our `spacetimedb_lib::http::Response` type, // which has a stable BSATN encoding to pass across the WASM boundary. - let response = st_http::Response::from(response); + let response = convert_http_response(response); Ok((response, body)) } @@ -692,6 +669,76 @@ impl InstanceEnv { /// Value chosen arbitrarily by pgoldman 2025-11-18, based on little more than a vague guess. const HTTP_DEFAULT_TIMEOUT: Duration = Duration::from_millis(500); +fn convert_http_request(request: st_http::Request) -> http::Result<(http::request::Parts, Option)> { + let st_http::Request { + method, + headers, + timeout, + uri, + version, + } = request; + + let (mut request, ()) = http::Request::new(()).into_parts(); + request.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(method) => http::Method::from_bytes(method.as_bytes()).expect("Invalid HTTP method"), + }; + request.uri = uri.try_into()?; + request.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, + }; + request.headers = headers + .into_iter() + .map(|(k, v)| Ok((k.into_string().try_into()?, v.into_vec().try_into()?))) + .collect::>()?; + + let timeout = timeout.map(|d| d.to_duration_saturating()); + + Ok((request, timeout)) +} + +fn convert_http_response(response: http::response::Parts) -> st_http::Response { + let http::response::Parts { + extensions, + headers, + status, + version, + .. + } = response; + + // there's a good chance that reqwest inserted some extensions into this request, + // but we can't control that and don't care much about it. + let _ = extensions; + + st_http::Response { + headers: headers + .into_iter() + .map(|(k, v)| (k.map(|k| k.as_str().into()), v.as_bytes().into())) + .collect(), + 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:?}"), + }, + code: status.as_u16(), + } +} + impl TxSlot { /// Sets the slot to `tx`, ensuring that there was no tx before. pub fn set_raw(&mut self, tx: MutTxId) { diff --git a/crates/lib/Cargo.toml b/crates/lib/Cargo.toml index d0f13095c5c..9eadb07888a 100644 --- a/crates/lib/Cargo.toml +++ b/crates/lib/Cargo.toml @@ -43,7 +43,6 @@ chrono.workspace = true derive_more.workspace = true enum-as-inner.workspace = true hex.workspace = true -http.workspace = true itertools.workspace = true log.workspace = true serde = { workspace = true, optional = true } diff --git a/crates/lib/src/http.rs b/crates/lib/src/http.rs index ce0c46e981f..105283742a2 100644 --- a/crates/lib/src/http.rs +++ b/crates/lib/src/http.rs @@ -1,14 +1,13 @@ //! `SpacetimeType`-ified HTTP request, response and error types, //! for use in the procedure HTTP API. //! -//! The types here are all mirrors of various types within the [`http`] crate. +//! The types here are all mirrors of various types within the `http` crate. //! That crate's types don't have stable representations or `pub`lic interiors, //! so we're forced to define our own representation for the SATS serialization. //! These types are that representation. //! //! Users aren't intended to interact with these types, -//! except [`Timeout`] and [`Error`], which are re-exported from the `bindings` crate. -//! Our user-facing APIs should use the [`http`] crate's types directly, and convert to and from these types internally. +//! Our user-facing APIs should use the `http` crate's types directly, and convert to and from these types internally. //! //! These types are used in BSATN encoding for interchange between the SpacetimeDB host //! and guest WASM modules in the `procedure_http_request` ABI call. @@ -22,97 +21,20 @@ use spacetimedb_sats::{time_duration::TimeDuration, SpacetimeType}; /// Represents an HTTP request which can be made from a procedure running in a SpacetimeDB database. -/// -/// Construct instances of this type by converting from [`http::Request`]. -/// Note that all extensions to [`http::Request`] save for [`Timeout`] are ignored. #[derive(Clone, SpacetimeType)] -#[sats(crate = crate)] +#[sats(crate = crate, name = "HttpRequest")] pub struct Request { - method: Method, - headers: Headers, - timeout: Option, - /// A valid URI, sourced from an already-validated [`http::Uri`]. - uri: String, - version: Version, -} - -impl From for Request { - fn from(parts: http::request::Parts) -> Request { - let http::request::Parts { - method, - uri, - version, - headers, - mut extensions, - .. - } = parts; - - let timeout = extensions.remove::(); - if !extensions.is_empty() { - log::warn!("Converting HTTP `Request` with unrecognized extensions"); - } - Request { - method: method.into(), - headers: headers.into(), - timeout, - uri: uri.to_string(), - version: version.into(), - } - } -} - -impl TryFrom for http::request::Parts { - type Error = http::Error; - fn try_from(req: Request) -> http::Result { - let Request { - method, - headers, - timeout, - uri, - version, - } = req; - let (mut request, ()) = http::Request::new(()).into_parts(); - request.method = method.into(); - request.uri = uri.try_into()?; - request.version = version.into(); - request.headers = headers.try_into()?; - - if let Some(timeout) = timeout { - request.extensions.insert(timeout); - } - - Ok(request) - } -} - -/// An HTTP extension to specify a timeout for requests made by a procedure running in a SpacetimeDB database. -/// -/// Pass an instance of this type to [`http::request::Builder::extension`] to set a timeout on a request. -/// -/// This timeout applies to the entire request, -/// from when the headers are first sent to when the response body is fully downloaded. -/// This is sometimes called a total timeout, the sum of the connect timeout and the read timeout. -#[derive(Clone, SpacetimeType, Copy, PartialEq, Eq)] -#[sats(crate = crate)] -pub struct Timeout { - pub timeout: TimeDuration, -} - -impl From for Timeout { - fn from(timeout: TimeDuration) -> Timeout { - Timeout { timeout } - } -} - -impl From for TimeDuration { - fn from(Timeout { timeout }: Timeout) -> TimeDuration { - timeout - } + pub method: Method, + pub headers: Headers, + pub timeout: Option, + /// A valid URI, sourced from an already-validated `http::Uri`. + pub uri: String, + pub version: Version, } /// Represents an HTTP method. #[derive(Clone, SpacetimeType, PartialEq, Eq)] -#[sats(crate = crate)] +#[sats(crate = crate, name = "HttpMethod")] pub enum Method { Get, Head, @@ -126,55 +48,9 @@ pub enum Method { Extension(String), } -impl Method { - pub const GET: Method = Method::Get; - pub const HEAD: Method = Method::Head; - pub const POST: Method = Method::Post; - pub const PUT: Method = Method::Put; - pub const DELETE: Method = Method::Delete; - pub const CONNECT: Method = Method::Connect; - pub const OPTIONS: Method = Method::Options; - pub const TRACE: Method = Method::Trace; - pub const PATCH: Method = Method::Patch; -} - -impl From for Method { - fn from(method: http::Method) -> Method { - match method { - http::Method::GET => Method::Get, - http::Method::HEAD => Method::Head, - http::Method::POST => Method::Post, - http::Method::PUT => Method::Put, - http::Method::DELETE => Method::Delete, - http::Method::CONNECT => Method::Connect, - http::Method::OPTIONS => Method::Options, - http::Method::TRACE => Method::Trace, - http::Method::PATCH => Method::Patch, - _ => Method::Extension(method.to_string()), - } - } -} - -impl From for http::Method { - fn from(method: Method) -> http::Method { - match method { - Method::Get => http::Method::GET, - Method::Head => http::Method::HEAD, - Method::Post => http::Method::POST, - Method::Put => http::Method::PUT, - Method::Delete => http::Method::DELETE, - Method::Connect => http::Method::CONNECT, - Method::Options => http::Method::OPTIONS, - Method::Trace => http::Method::TRACE, - Method::Patch => http::Method::PATCH, - Method::Extension(method) => http::Method::from_bytes(method.as_bytes()).expect("Invalid HTTP method"), - } - } -} - /// An HTTP version. #[derive(Clone, SpacetimeType, PartialEq, Eq)] -#[sats(crate = crate)] +#[sats(crate = crate, name = "HttpVersion")] pub enum Version { Http09, Http10, @@ -183,185 +59,75 @@ pub enum Version { Http3, } -impl From for Version { - fn from(version: http::Version) -> Version { - match version { - http::Version::HTTP_09 => Version::Http09, - http::Version::HTTP_10 => Version::Http10, - http::Version::HTTP_11 => Version::Http11, - http::Version::HTTP_2 => Version::Http2, - http::Version::HTTP_3 => Version::Http3, - _ => unreachable!("Unknown HTTP version: {version:?}"), - } - } -} - -impl From for http::Version { - fn from(version: Version) -> http::Version { - match version { - Version::Http09 => http::Version::HTTP_09, - Version::Http10 => http::Version::HTTP_10, - Version::Http11 => http::Version::HTTP_11, - Version::Http2 => http::Version::HTTP_2, - Version::Http3 => http::Version::HTTP_3, - } - } -} - /// A set of HTTP headers. -/// -/// Construct this by converting from a [`http::HeaderMap`]. #[derive(Clone, SpacetimeType)] -#[sats(crate = crate)] +#[sats(crate = crate, name = "HttpHeaders")] pub struct Headers { // SATS doesn't (and won't) have a multimap type, so just use an array of pairs for the ser/de format. entries: Box<[HttpHeaderPair]>, } -impl From> for Headers { - fn from(value: http::HeaderMap) -> Headers { - Headers { - entries: value - .into_iter() - .map(|(name, value)| HttpHeaderPair { - name: name.map(|name| name.to_string()).unwrap_or_default(), - value: value.into(), - }) - .collect(), - } - } -} - -impl TryFrom for http::HeaderMap { - type Error = http::Error; - fn try_from(headers: Headers) -> http::Result { - let Headers { entries } = headers; - let mut new_headers = http::HeaderMap::with_capacity(entries.len() / 2); - for HttpHeaderPair { name, value } in entries { - new_headers.insert(http::HeaderName::try_from(name)?, value.try_into()?); - } - Ok(new_headers) - } +// `http::header::IntoIter` only returns the `HeaderName` for the first +// `HeaderValue` with that name, so we have to manually assign the names. +struct HeaderIter { + prev: Option<(Box, T)>, + inner: I, } -#[derive(Clone, SpacetimeType)] -#[sats(crate = crate)] -struct HttpHeaderPair { - /// A valid HTTP header name, sourced from an already-validated [`http::HeaderName`]. - name: String, - value: HeaderValue, -} +impl Iterator for HeaderIter +where + I: Iterator>, T)>, +{ + type Item = (Box, T); -/// A valid HTTP header value, sourced from an already-validated [`http::HeaderValue`]. -#[derive(Clone, SpacetimeType)] -#[sats(crate = crate)] -struct HeaderValue { - bytes: Box<[u8]>, - is_sensitive: bool, -} - -impl From for HeaderValue { - fn from(value: http::HeaderValue) -> HeaderValue { - HeaderValue { - is_sensitive: value.is_sensitive(), - bytes: value.as_bytes().into(), - } + fn next(&mut self) -> Option { + let (prev_k, prev_v) = self + .prev + .take() + .or_else(|| self.inner.next().map(|(k, v)| (k.unwrap(), v)))?; + self.prev = self + .inner + .next() + .map(|(next_k, next_v)| (next_k.unwrap_or_else(|| prev_k.clone()), next_v)); + Some((prev_k, prev_v)) } -} -impl TryFrom for http::HeaderValue { - type Error = http::Error; - fn try_from(value: HeaderValue) -> http::Result { - let mut new_value = http::HeaderValue::from_bytes(&value.bytes)?; - new_value.set_sensitive(value.is_sensitive); - Ok(new_value) + fn size_hint(&self) -> (usize, Option) { + self.inner.size_hint() } } -#[derive(Clone, SpacetimeType)] -#[sats(crate = crate)] -pub struct Response { - inner: HttpResponse, -} - -impl TryFrom for http::response::Parts { - type Error = http::Error; - fn try_from(response: Response) -> http::Result { - let Response { - inner: HttpResponse { headers, version, code }, - } = response; - - let (mut response, ()) = http::Response::new(()).into_parts(); - response.version = version.into(); - response.status = http::StatusCode::from_u16(code)?; - response.headers = headers.try_into()?; - Ok(response) +impl FromIterator<(Option>, Box<[u8]>)> for Headers { + fn from_iter>, Box<[u8]>)>>(iter: T) -> Self { + let inner = iter.into_iter(); + let entries = HeaderIter { prev: None, inner } + .map(|(name, value)| HttpHeaderPair { name, value }) + .collect(); + Self { entries } } } -impl From for Response { - fn from(response: http::response::Parts) -> Response { - let http::response::Parts { - extensions, - headers, - status, - version, - .. - } = response; - if !extensions.is_empty() { - log::warn!("Converting HTTP `Response` with unrecognized extensions"); - } - Response { - inner: HttpResponse { - headers: headers.into(), - version: version.into(), - code: status.as_u16(), - }, - } +impl Headers { + #[allow(clippy::should_implement_trait)] + pub fn into_iter(self) -> impl Iterator, Box<[u8]>)> { + IntoIterator::into_iter(self.entries).map(|HttpHeaderPair { name, value }| (name, value)) } } #[derive(Clone, SpacetimeType)] -#[sats(crate = crate)] -struct HttpResponse { - headers: Headers, - version: Version, - /// A valid HTTP response status code, sourced from an already-validated [`http::StatusCode`]. - code: u16, -} - -/// Errors that may arise from HTTP calls. -#[derive(Clone, SpacetimeType, Debug)] -#[sats(crate = crate)] -pub struct Error { - /// A string message describing the error. - /// - /// It would be nice if we could store a more interesting object here, - /// ideally a type-erased `dyn Trait` cause, - /// rather than just a string, similar to how `anyhow` does. - /// This is not possible because we need to serialize `Error` for transport to WASM, - /// meaning it must have a concrete static type. - /// `reqwest::Error`, which is the source for these, - /// is type-erased enough that the best we can do (at least, the best we can do easily) - /// is to eagerly string-ify the 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) - } +#[sats(crate = crate, name = "HttpHeaderPair")] +struct HttpHeaderPair { + /// A valid HTTP header name, sourced from an already-validated `http::HeaderName`. + name: Box, + /// A valid HTTP header value, sourced from an already-validated `http::HeaderValue`. + value: Box<[u8]>, } -impl std::error::Error for Error {} - -impl Error { - pub fn from_string(message: String) -> Self { - Error { message } - } - - pub fn from_display(t: &impl std::fmt::Display) -> Self { - Self::from_string(format!("{t}")) - } +#[derive(Clone, SpacetimeType)] +#[sats(crate = crate, name = "HttpResponse")] +pub struct Response { + pub headers: Headers, + pub version: Version, + /// A valid HTTP response status code, sourced from an already-validated `http::StatusCode`. + pub code: u16, } diff --git a/crates/sats/src/time_duration.rs b/crates/sats/src/time_duration.rs index f6d932c4bd8..ff1f4a2e32a 100644 --- a/crates/sats/src/time_duration.rs +++ b/crates/sats/src/time_duration.rs @@ -40,10 +40,11 @@ impl TimeDuration { /// Returns `Err(abs(self) as Duration)` if `self` is negative. pub fn to_duration(self) -> Result { let micros = self.to_micros(); + let duration = Duration::from_micros(micros.unsigned_abs()); if micros >= 0 { - Ok(Duration::from_micros(micros as u64)) + Ok(duration) } else { - Err(Duration::from_micros((-micros) as u64)) + Err(duration) } } @@ -56,6 +57,11 @@ impl TimeDuration { } } + /// Converts `self` to `Duration`, clamping to 0 if negative. + pub fn to_duration_saturating(self) -> Duration { + self.to_duration().unwrap_or(Duration::ZERO) + } + /// Returns a positive `TimeDuration` with the magnitude of `self`. pub fn abs(self) -> Self { Self::from_micros(self.to_micros().saturating_abs())