Skip to main content

object_store/client/http/
connection.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18use 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/// An HTTP protocol error
33///
34/// Clients should return this when an HTTP request fails to be completed, e.g. because
35/// of a connection issue. This does **not** include HTTP requests that are return
36/// non 2xx Status Codes, as these should instead be returned as an [`HttpResponse`]
37/// with the appropriate status code set.
38#[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/// Identifies the kind of [`HttpError`]
47///
48/// This is used, among other things, to determine if a request can be retried
49#[derive(Debug, Copy, Clone, PartialEq, Eq)]
50#[non_exhaustive]
51pub enum HttpErrorKind {
52    /// An error occurred whilst connecting to the remote
53    ///
54    /// Will be automatically retried
55    Connect,
56    /// An error occurred whilst making the request
57    ///
58    /// Will be automatically retried
59    Request,
60    /// Request timed out
61    ///
62    /// Will be automatically retried if the request is idempotent
63    Timeout,
64    /// The request was aborted
65    ///
66    /// Will be automatically retried if the request is idempotent
67    Interrupted,
68    /// An error occurred whilst decoding the response
69    ///
70    /// Will not be automatically retried
71    Decode,
72    /// An unknown error occurred
73    ///
74    /// Will not be automatically retried
75    Unknown,
76}
77
78impl HttpError {
79    /// Create a new [`HttpError`] with the optional status code
80    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    /// Create a new [`HttpError`] with provided `source` error
89    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        // Reqwest error variants aren't great, attempt to refine them
111        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            // We strip URL as it will be included by RetryError if not sensitive
139            source: Box::new(e.without_url()),
140        }
141    }
142
143    /// Returns the [`HttpErrorKind`]
144    pub fn kind(&self) -> HttpErrorKind {
145        self.kind
146    }
147}
148
149/// An asynchronous function from a [`HttpRequest`] to a [`HttpResponse`].
150#[async_trait]
151pub trait HttpService: std::fmt::Debug + Send + Sync + 'static {
152    /// Perform [`HttpRequest`] returning [`HttpResponse`]
153    async fn call(&self, req: HttpRequest) -> Result<HttpResponse, HttpError>;
154}
155
156/// An HTTP client
157#[derive(Debug, Clone)]
158pub struct HttpClient(Arc<dyn HttpService>);
159
160impl HttpClient {
161    /// Create a new [`HttpClient`] from an [`HttpService`]
162    pub fn new(service: impl HttpService + 'static) -> Self {
163        Self(Arc::new(service))
164    }
165
166    /// Performs [`HttpRequest`] using this client
167    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                            // Disconnected due to a transitive drop of the receiver
272                            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
290/// A factory for [`HttpClient`]
291pub trait HttpConnector: std::fmt::Debug + Send + Sync + 'static {
292    /// Create a new [`HttpClient`] with the provided [`ClientOptions`]
293    fn connect(&self, options: &ClientOptions) -> crate::Result<HttpClient>;
294}
295
296/// [`HttpConnector`] using [`reqwest::Client`]
297#[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/// [`reqwest::Client`] connector that performs all I/O on the provided tokio
317/// [`Runtime`] (thread pool).
318///
319/// This adapter is most useful when you wish to segregate I/O from CPU bound
320/// work that may be happening on the [`Runtime`].
321///
322/// [`Runtime`]: tokio::runtime::Runtime
323///
324/// # Example: Spawning requests on separate runtime
325///
326/// ```
327/// # use std::sync::Arc;
328/// # use tokio::runtime::Runtime;
329/// # use object_store::azure::MicrosoftAzureBuilder;
330/// # use object_store::client::SpawnedReqwestConnector;
331/// # use object_store::ObjectStore;
332/// # fn get_io_runtime() -> Runtime {
333/// #   tokio::runtime::Builder::new_current_thread().build().unwrap()
334/// # }
335/// # fn main() -> Result<(), object_store::Error> {
336/// // create a tokio runtime for I/O.
337/// let io_runtime: Runtime = get_io_runtime();
338/// // configure a store using the runtime.
339/// let handle = io_runtime.handle().clone(); // get a handle to the same runtime
340/// let store: Arc<dyn ObjectStore> = Arc::new(
341///   MicrosoftAzureBuilder::new()
342///     .with_http_connector(SpawnedReqwestConnector::new(handle))
343///     .with_container_name("my_container")
344///     .with_account("my_account")
345///     .build()?
346///  );
347/// // any requests made using store will be spawned on the io_runtime
348/// # Ok(())
349/// # }
350/// ```
351#[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    /// Create a new [`SpawnedReqwestConnector`] with the provided [`Handle`] to
361    /// a tokio [`Runtime`]
362    ///
363    /// [`Runtime`]: tokio::runtime::Runtime
364    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}