object_store/client/http/
connection.rs1use crate::ClientOptions;
19#[cfg(feature = "reqwest")]
20use crate::client::HttpResponseBody;
21use crate::client::builder::{HttpRequestBuilder, RequestBuilderError};
22use crate::client::{HttpRequest, HttpResponse};
23use async_trait::async_trait;
24use http::{Method, Uri};
25#[cfg(feature = "reqwest")]
26use http_body_util::BodyExt;
27use std::error::Error;
28use std::sync::Arc;
29#[cfg(all(feature = "reqwest", not(target_arch = "wasm32")))]
30use tokio::runtime::Handle;
31
32#[derive(Debug, thiserror::Error)]
39#[error("HTTP error: {source}")]
40pub struct HttpError {
41 kind: HttpErrorKind,
42 #[source]
43 source: Box<dyn Error + Send + Sync>,
44}
45
46#[derive(Debug, Copy, Clone, PartialEq, Eq)]
50#[non_exhaustive]
51pub enum HttpErrorKind {
52 Connect,
56 Request,
60 Timeout,
64 Interrupted,
68 Decode,
72 Unknown,
76}
77
78impl HttpError {
79 pub fn new<E>(kind: HttpErrorKind, e: E) -> Self
81 where
82 E: Error + Send + Sync + 'static,
83 {
84 Self::new_boxed(kind, Box::new(e))
85 }
86
87 #[inline]
88 pub fn new_boxed(kind: HttpErrorKind, source: Box<dyn Error + Send + Sync>) -> Self {
90 Self { kind, source }
91 }
92
93 #[cfg(feature = "reqwest")]
94 pub(crate) fn reqwest(e: reqwest::Error) -> Self {
95 #[cfg(not(target_arch = "wasm32"))]
96 let is_connect = || e.is_connect();
97 #[cfg(target_arch = "wasm32")]
98 let is_connect = || false;
99
100 let mut kind = if e.is_timeout() {
101 HttpErrorKind::Timeout
102 } else if is_connect() {
103 HttpErrorKind::Connect
104 } else if e.is_decode() {
105 HttpErrorKind::Decode
106 } else {
107 HttpErrorKind::Unknown
108 };
109
110 let mut source = e.source();
112 while kind == HttpErrorKind::Unknown {
113 if let Some(e) = source {
114 if let Some(e) = e.downcast_ref::<hyper::Error>() {
115 if e.is_closed() || e.is_incomplete_message() || e.is_body_write_aborted() {
116 kind = HttpErrorKind::Request;
117 } else if e.is_timeout() {
118 kind = HttpErrorKind::Timeout;
119 }
120 }
121 if let Some(e) = e.downcast_ref::<std::io::Error>() {
122 match e.kind() {
123 std::io::ErrorKind::TimedOut => kind = HttpErrorKind::Timeout,
124 std::io::ErrorKind::ConnectionAborted
125 | std::io::ErrorKind::ConnectionReset
126 | std::io::ErrorKind::BrokenPipe
127 | std::io::ErrorKind::UnexpectedEof => kind = HttpErrorKind::Interrupted,
128 _ => {}
129 }
130 }
131 source = e.source();
132 } else {
133 break;
134 }
135 }
136 Self {
137 kind,
138 source: Box::new(e.without_url()),
140 }
141 }
142
143 pub fn kind(&self) -> HttpErrorKind {
145 self.kind
146 }
147}
148
149#[async_trait]
151pub trait HttpService: std::fmt::Debug + Send + Sync + 'static {
152 async fn call(&self, req: HttpRequest) -> Result<HttpResponse, HttpError>;
154}
155
156#[derive(Debug, Clone)]
158pub struct HttpClient(Arc<dyn HttpService>);
159
160impl HttpClient {
161 pub fn new(service: impl HttpService + 'static) -> Self {
163 Self(Arc::new(service))
164 }
165
166 pub async fn execute(&self, request: HttpRequest) -> Result<HttpResponse, HttpError> {
168 self.0.call(request).await
169 }
170
171 #[allow(unused)]
172 pub(crate) fn get<U>(&self, url: U) -> HttpRequestBuilder
173 where
174 U: TryInto<Uri>,
175 U::Error: Into<RequestBuilderError>,
176 {
177 self.request(Method::GET, url)
178 }
179
180 #[allow(unused)]
181 pub(crate) fn post<U>(&self, url: U) -> HttpRequestBuilder
182 where
183 U: TryInto<Uri>,
184 U::Error: Into<RequestBuilderError>,
185 {
186 self.request(Method::POST, url)
187 }
188
189 #[allow(unused)]
190 pub(crate) fn put<U>(&self, url: U) -> HttpRequestBuilder
191 where
192 U: TryInto<Uri>,
193 U::Error: Into<RequestBuilderError>,
194 {
195 self.request(Method::PUT, url)
196 }
197
198 #[allow(unused)]
199 pub(crate) fn delete<U>(&self, url: U) -> HttpRequestBuilder
200 where
201 U: TryInto<Uri>,
202 U::Error: Into<RequestBuilderError>,
203 {
204 self.request(Method::DELETE, url)
205 }
206
207 pub(crate) fn request<U>(&self, method: Method, url: U) -> HttpRequestBuilder
208 where
209 U: TryInto<Uri>,
210 U::Error: Into<RequestBuilderError>,
211 {
212 HttpRequestBuilder::new(self.clone())
213 .uri(url)
214 .method(method)
215 }
216}
217
218#[async_trait]
219#[cfg(all(feature = "reqwest", not(target_arch = "wasm32")))]
220impl HttpService for reqwest::Client {
221 async fn call(&self, req: HttpRequest) -> Result<HttpResponse, HttpError> {
222 let (parts, body) = req.into_parts();
223
224 let url = parts.uri.to_string().parse().unwrap();
225 let mut req = reqwest::Request::new(parts.method, url);
226 *req.headers_mut() = parts.headers;
227 *req.body_mut() = Some(body.into_reqwest());
228
229 let r = self.execute(req).await.map_err(HttpError::reqwest)?;
230 let res: http::Response<reqwest::Body> = r.into();
231 let (parts, body) = res.into_parts();
232
233 let body = HttpResponseBody::new(body.map_err(HttpError::reqwest));
234 Ok(HttpResponse::from_parts(parts, body))
235 }
236}
237
238#[async_trait]
239#[cfg(all(feature = "reqwest", target_arch = "wasm32", target_os = "unknown"))]
240impl HttpService for reqwest::Client {
241 async fn call(&self, req: HttpRequest) -> Result<HttpResponse, HttpError> {
242 use futures_channel::{mpsc, oneshot};
243 use futures_util::{SinkExt, StreamExt, TryStreamExt};
244 use http_body_util::{Empty, StreamBody};
245 use wasm_bindgen_futures::spawn_local;
246
247 let (parts, body) = req.into_parts();
248 let url = parts.uri.to_string().parse().unwrap();
249 let mut req = reqwest::Request::new(parts.method, url);
250 *req.headers_mut() = parts.headers;
251 *req.body_mut() = Some(body.into_reqwest());
252
253 let (mut tx, rx) = mpsc::channel(1);
254 let (tx_parts, rx_parts) = oneshot::channel();
255 let res_fut = self.execute(req);
256
257 spawn_local(async move {
258 match res_fut.await.map_err(HttpError::reqwest) {
259 Err(err) => {
260 let _ = tx_parts.send(Err(err));
261 drop(tx);
262 }
263 Ok(res) => {
264 let (mut parts, _) = http::Response::new(Empty::<()>::new()).into_parts();
265 parts.headers = res.headers().clone();
266 parts.status = res.status();
267 let _ = tx_parts.send(Ok(parts));
268 let mut stream = res.bytes_stream().map_err(HttpError::reqwest);
269 while let Some(chunk) = stream.next().await {
270 if let Err(_e) = tx.send(chunk).await {
271 break;
273 }
274 }
275 }
276 }
277 });
278
279 let parts = rx_parts.await.unwrap()?;
280 let safe_stream = rx.map(|chunk| {
281 let frame = hyper::body::Frame::data(chunk?);
282 Ok(frame)
283 });
284 let body = HttpResponseBody::new(StreamBody::new(safe_stream));
285
286 Ok(HttpResponse::from_parts(parts, body))
287 }
288}
289
290pub trait HttpConnector: std::fmt::Debug + Send + Sync + 'static {
292 fn connect(&self, options: &ClientOptions) -> crate::Result<HttpClient>;
294}
295
296#[derive(Debug, Default)]
298#[allow(missing_copy_implementations)]
299#[cfg(all(
300 feature = "reqwest",
301 not(all(target_arch = "wasm32", target_os = "wasi"))
302))]
303pub struct ReqwestConnector {}
304
305#[cfg(all(
306 feature = "reqwest",
307 not(all(target_arch = "wasm32", target_os = "wasi"))
308))]
309impl HttpConnector for ReqwestConnector {
310 fn connect(&self, options: &ClientOptions) -> crate::Result<HttpClient> {
311 let client = options.client()?;
312 Ok(HttpClient::new(client))
313 }
314}
315
316#[derive(Debug)]
352#[allow(missing_copy_implementations)]
353#[cfg(all(feature = "reqwest", not(target_arch = "wasm32")))]
354pub struct SpawnedReqwestConnector {
355 runtime: Handle,
356}
357
358#[cfg(all(feature = "reqwest", not(target_arch = "wasm32")))]
359impl SpawnedReqwestConnector {
360 pub fn new(runtime: Handle) -> Self {
365 Self { runtime }
366 }
367}
368
369#[cfg(all(feature = "reqwest", not(target_arch = "wasm32")))]
370impl HttpConnector for SpawnedReqwestConnector {
371 fn connect(&self, options: &ClientOptions) -> crate::Result<HttpClient> {
372 let spawn_service = super::SpawnService::new(options.client()?, self.runtime.clone());
373 Ok(HttpClient::new(spawn_service))
374 }
375}
376
377#[cfg(all(feature = "reqwest", target_arch = "wasm32", target_os = "wasi"))]
378pub(crate) fn http_connector(
379 custom: Option<Arc<dyn HttpConnector>>,
380) -> crate::Result<Arc<dyn HttpConnector>> {
381 match custom {
382 Some(x) => Ok(x),
383 None => Err(crate::Error::NotSupported {
384 source: "reqwest is not supported on the WASI architecture; \
385 supply a custom HttpConnector via `.with_http_connector(...)`"
386 .to_string()
387 .into(),
388 }),
389 }
390}
391
392#[cfg(all(not(feature = "reqwest"), target_arch = "wasm32", target_os = "wasi"))]
393pub(crate) fn http_connector(
394 custom: Option<Arc<dyn HttpConnector>>,
395) -> crate::Result<Arc<dyn HttpConnector>> {
396 match custom {
397 Some(x) => Ok(x),
398 None => Err(crate::Error::NotSupported {
399 source: "WASI architectures must provide an HttpConnector"
400 .to_string()
401 .into(),
402 }),
403 }
404}
405
406#[cfg(all(
407 feature = "reqwest",
408 not(all(target_arch = "wasm32", target_os = "wasi"))
409))]
410pub(crate) fn http_connector(
411 custom: Option<Arc<dyn HttpConnector>>,
412) -> crate::Result<Arc<dyn HttpConnector>> {
413 match custom {
414 Some(x) => Ok(x),
415 None => Ok(Arc::new(ReqwestConnector {})),
416 }
417}
418
419#[cfg(all(
420 not(feature = "reqwest"),
421 not(all(target_arch = "wasm32", target_os = "wasi"))
422))]
423pub(crate) fn http_connector(
424 custom: Option<Arc<dyn HttpConnector>>,
425) -> crate::Result<Arc<dyn HttpConnector>> {
426 match custom {
427 Some(x) => Ok(x),
428 None => Err(crate::Error::NotSupported {
429 source: "no built-in HTTP transport: enable the `reqwest` feature \
430 or supply a custom HttpConnector via `.with_http_connector(...)`"
431 .to_string()
432 .into(),
433 }),
434 }
435}