|
6 | 6 | //! while [`send`](HttpClient::send) allows more complex requests with headers, bodies and other methods. |
7 | 7 |
|
8 | 8 | use bytes::Bytes; |
9 | | -pub use http::{Request, Response}; |
10 | | -pub use spacetimedb_lib::http::{Error, Timeout}; |
11 | 9 |
|
12 | 10 | use crate::{ |
13 | 11 | rt::{read_bytes_source_as, read_bytes_source_into}, |
14 | 12 | IterBuf, |
15 | 13 | }; |
16 | | -use spacetimedb_lib::{bsatn, http as st_http}; |
| 14 | +use spacetimedb_lib::{bsatn, http as st_http, TimeDuration}; |
| 15 | + |
| 16 | +pub type Request<T = Body> = http::Request<T>; |
| 17 | + |
| 18 | +pub type Response<T = Body> = http::Response<T>; |
17 | 19 |
|
18 | 20 | /// Allows performing HTTP requests via [`HttpClient::send`] and [`HttpClient::get`]. |
19 | 21 | /// |
@@ -43,8 +45,7 @@ impl HttpClient { |
43 | 45 | /// and a timeout of 100 milliseconds, then treat the response as a string and log it: |
44 | 46 | /// |
45 | 47 | /// ```norun |
46 | | - /// # use spacetimedb::{procedure, ProcedureContext}; |
47 | | - /// # use spacetimedb::http::{Request, Timeout}; |
| 48 | + /// # use spacetimedb::{procedure, ProcedureContext, http::Timeout}; |
48 | 49 | /// # use std::time::Duration; |
49 | 50 | /// # #[procedure] |
50 | 51 | /// # fn post_somewhere(ctx: &mut ProcedureContext) { |
@@ -73,25 +74,24 @@ impl HttpClient { |
73 | 74 | /// # } |
74 | 75 | /// |
75 | 76 | /// ``` |
76 | | - pub fn send<B: Into<Body>>(&self, request: Request<B>) -> Result<Response<Body>, Error> { |
| 77 | + pub fn send<B: Into<Body>>(&self, request: http::Request<B>) -> Result<Response, Error> { |
77 | 78 | let (request, body) = request.map(Into::into).into_parts(); |
78 | | - let request = st_http::Request::from(request); |
| 79 | + let request = convert_request(request); |
79 | 80 | let request = bsatn::to_vec(&request).expect("Failed to BSATN-serialize `spacetimedb_lib::http::Request`"); |
80 | 81 |
|
81 | 82 | match spacetimedb_bindings_sys::procedure::http_request(&request, &body.into_bytes()) { |
82 | 83 | Ok((response_source, body_source)) => { |
83 | 84 | let response = read_bytes_source_as::<st_http::Response>(response_source); |
84 | | - let response = |
85 | | - http::response::Parts::try_from(response).expect("Invalid http response returned from host"); |
| 85 | + let response = convert_response(response).expect("Invalid http response returned from host"); |
86 | 86 | let mut buf = IterBuf::take(); |
87 | 87 | read_bytes_source_into(body_source, &mut buf); |
88 | 88 | let body = Body::from_bytes(buf.clone()); |
89 | 89 |
|
90 | 90 | Ok(http::Response::from_parts(response, body)) |
91 | 91 | } |
92 | 92 | Err(err_source) => { |
93 | | - let error = read_bytes_source_as::<st_http::Error>(err_source); |
94 | | - Err(error) |
| 93 | + let message = read_bytes_source_as::<String>(err_source); |
| 94 | + Err(Error { message }) |
95 | 95 | } |
96 | 96 | } |
97 | 97 | } |
@@ -121,17 +121,79 @@ impl HttpClient { |
121 | 121 | /// } |
122 | 122 | /// # } |
123 | 123 | /// ``` |
124 | | - pub fn get(&self, uri: impl TryInto<http::Uri, Error: Into<http::Error>>) -> Result<Response<Body>, Error> { |
| 124 | + pub fn get(&self, uri: impl TryInto<http::Uri, Error: Into<http::Error>>) -> Result<Response, Error> { |
125 | 125 | self.send( |
126 | 126 | http::Request::builder() |
127 | | - .method("GET") |
| 127 | + .method(http::Method::GET) |
128 | 128 | .uri(uri) |
129 | | - .body(Body::empty()) |
130 | | - .map_err(|err| Error::from_display(&err))?, |
| 129 | + .body(Body::empty())?, |
131 | 130 | ) |
132 | 131 | } |
133 | 132 | } |
134 | 133 |
|
| 134 | +fn convert_request(parts: http::request::Parts) -> st_http::Request { |
| 135 | + let http::request::Parts { |
| 136 | + method, |
| 137 | + uri, |
| 138 | + version, |
| 139 | + headers, |
| 140 | + mut extensions, |
| 141 | + .. |
| 142 | + } = parts; |
| 143 | + |
| 144 | + let timeout = extensions.remove::<Timeout>(); |
| 145 | + if !extensions.is_empty() { |
| 146 | + log::warn!("Converting HTTP `Request` with unrecognized extensions"); |
| 147 | + } |
| 148 | + st_http::Request { |
| 149 | + method: match method { |
| 150 | + http::Method::GET => st_http::Method::Get, |
| 151 | + http::Method::HEAD => st_http::Method::Head, |
| 152 | + http::Method::POST => st_http::Method::Post, |
| 153 | + http::Method::PUT => st_http::Method::Put, |
| 154 | + http::Method::DELETE => st_http::Method::Delete, |
| 155 | + http::Method::CONNECT => st_http::Method::Connect, |
| 156 | + http::Method::OPTIONS => st_http::Method::Options, |
| 157 | + http::Method::TRACE => st_http::Method::Trace, |
| 158 | + http::Method::PATCH => st_http::Method::Patch, |
| 159 | + _ => st_http::Method::Extension(method.to_string()), |
| 160 | + }, |
| 161 | + headers: headers |
| 162 | + .into_iter() |
| 163 | + .map(|(k, v)| (k.map(|k| k.as_str().into()), v.as_bytes().into())) |
| 164 | + .collect(), |
| 165 | + timeout: timeout.map(Into::into), |
| 166 | + uri: uri.to_string(), |
| 167 | + version: match version { |
| 168 | + http::Version::HTTP_09 => st_http::Version::Http09, |
| 169 | + http::Version::HTTP_10 => st_http::Version::Http10, |
| 170 | + http::Version::HTTP_11 => st_http::Version::Http11, |
| 171 | + http::Version::HTTP_2 => st_http::Version::Http2, |
| 172 | + http::Version::HTTP_3 => st_http::Version::Http3, |
| 173 | + _ => unreachable!("Unknown HTTP version: {version:?}"), |
| 174 | + }, |
| 175 | + } |
| 176 | +} |
| 177 | + |
| 178 | +fn convert_response(response: st_http::Response) -> http::Result<http::response::Parts> { |
| 179 | + let st_http::Response { headers, version, code } = response; |
| 180 | + |
| 181 | + let (mut response, ()) = http::Response::new(()).into_parts(); |
| 182 | + response.version = match version { |
| 183 | + st_http::Version::Http09 => http::Version::HTTP_09, |
| 184 | + st_http::Version::Http10 => http::Version::HTTP_10, |
| 185 | + st_http::Version::Http11 => http::Version::HTTP_11, |
| 186 | + st_http::Version::Http2 => http::Version::HTTP_2, |
| 187 | + st_http::Version::Http3 => http::Version::HTTP_3, |
| 188 | + }; |
| 189 | + response.status = http::StatusCode::from_u16(code)?; |
| 190 | + response.headers = headers |
| 191 | + .into_iter() |
| 192 | + .map(|(k, v)| Ok((k.into_string().try_into()?, v.into_vec().try_into()?))) |
| 193 | + .collect::<http::Result<_>>()?; |
| 194 | + Ok(response) |
| 195 | +} |
| 196 | + |
135 | 197 | /// Represents the body of an HTTP request or response. |
136 | 198 | pub struct Body { |
137 | 199 | inner: BodyInner, |
@@ -208,3 +270,58 @@ impl_body_from_bytes!(_unit: () => Bytes::new()); |
208 | 270 | enum BodyInner { |
209 | 271 | Bytes(Bytes), |
210 | 272 | } |
| 273 | + |
| 274 | +/// An HTTP extension to specify a timeout for requests made by a procedure running in a SpacetimeDB database. |
| 275 | +/// |
| 276 | +/// Pass an instance of this type to [`http::request::Builder::extension`] to set a timeout on a request. |
| 277 | +/// |
| 278 | +/// This timeout applies to the entire request, |
| 279 | +/// from when the headers are first sent to when the response body is fully downloaded. |
| 280 | +/// This is sometimes called a total timeout, the sum of the connect timeout and the read timeout. |
| 281 | +#[derive(Clone, Copy, PartialEq, Eq)] |
| 282 | +pub struct Timeout(pub TimeDuration); |
| 283 | + |
| 284 | +impl From<TimeDuration> for Timeout { |
| 285 | + fn from(timeout: TimeDuration) -> Timeout { |
| 286 | + Timeout(timeout) |
| 287 | + } |
| 288 | +} |
| 289 | + |
| 290 | +impl From<Timeout> for TimeDuration { |
| 291 | + fn from(Timeout(timeout): Timeout) -> TimeDuration { |
| 292 | + timeout |
| 293 | + } |
| 294 | +} |
| 295 | + |
| 296 | +/// An error that may arise from an HTTP call. |
| 297 | +#[derive(Clone, Debug)] |
| 298 | +pub struct Error { |
| 299 | + /// A string message describing the error. |
| 300 | + /// |
| 301 | + /// It would be nice if we could store a more interesting object here, |
| 302 | + /// ideally a type-erased `dyn Trait` cause, |
| 303 | + /// rather than just a string, similar to how `anyhow` does. |
| 304 | + /// This is not possible because we need to serialize `Error` for transport to WASM, |
| 305 | + /// meaning it must have a concrete static type. |
| 306 | + /// `reqwest::Error`, which is the source for these, |
| 307 | + /// is type-erased enough that the best we can do (at least, the best we can do easily) |
| 308 | + /// is to eagerly string-ify the error. |
| 309 | + message: String, |
| 310 | +} |
| 311 | + |
| 312 | +impl std::fmt::Display for Error { |
| 313 | + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { |
| 314 | + let Error { message } = self; |
| 315 | + f.write_str(message) |
| 316 | + } |
| 317 | +} |
| 318 | + |
| 319 | +impl std::error::Error for Error {} |
| 320 | + |
| 321 | +impl From<http::Error> for Error { |
| 322 | + fn from(err: http::Error) -> Self { |
| 323 | + Error { |
| 324 | + message: err.to_string(), |
| 325 | + } |
| 326 | + } |
| 327 | +} |
0 commit comments