Skip to main content

opendal_http_transport_reqwest/
lib.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
18#![doc = include_str!("../README.md")]
19#![cfg_attr(docsrs, feature(doc_cfg))]
20#![cfg_attr(docsrs, doc(auto_cfg))]
21#![deny(missing_docs)]
22
23use std::fmt::{Debug, Formatter};
24use std::future;
25use std::mem;
26use std::sync::LazyLock;
27
28use futures::TryStreamExt;
29use http::Request;
30use http::Response;
31use opendal_core::Buffer;
32use opendal_core::Error;
33use opendal_core::ErrorKind;
34use opendal_core::HttpBody;
35use opendal_core::HttpTransport;
36use opendal_core::HttpTransporter;
37use opendal_core::Result;
38use opendal_core::raw::parse_content_encoding;
39use opendal_core::raw::parse_content_length;
40
41static DEFAULT_REQWEST_TRANSPORT: LazyLock<ReqwestTransport> =
42    LazyLock::new(|| ReqwestTransport::new(reqwest::Client::new()));
43
44/// A HTTP transport with [`reqwest::Client`].
45#[derive(Clone)]
46pub struct ReqwestTransport {
47    client: reqwest::Client,
48}
49
50impl Debug for ReqwestTransport {
51    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
52        f.debug_struct("ReqwestTransport").finish()
53    }
54}
55
56impl Default for ReqwestTransport {
57    fn default() -> Self {
58        DEFAULT_REQWEST_TRANSPORT.clone()
59    }
60}
61
62impl From<reqwest::Client> for ReqwestTransport {
63    fn from(client: reqwest::Client) -> Self {
64        Self::new(client)
65    }
66}
67
68impl ReqwestTransport {
69    /// Create a new transport from a [`reqwest::Client`].
70    pub fn new(client: reqwest::Client) -> Self {
71        Self { client }
72    }
73}
74
75impl HttpTransport for ReqwestTransport {
76    async fn fetch(&self, req: Request<Buffer>) -> Result<Response<HttpBody>> {
77        // Uri stores all string alike data in `Bytes` which means
78        // the clone here is cheap.
79        let uri = req.uri().clone();
80        let is_head = req.method() == http::Method::HEAD;
81
82        let (parts, body) = req.into_parts();
83
84        let url = reqwest::Url::parse(&uri.to_string()).map_err(|err| {
85            Error::new(ErrorKind::Unexpected, "request url is invalid")
86                .with_operation("reqwest::fetch")
87                .with_context("url", uri.to_string())
88                .set_source(err)
89        })?;
90
91        let mut req_builder = self
92            .client
93            .request(parts.method, url)
94            .headers(parts.headers);
95
96        // Client under wasm doesn't support set version.
97        #[cfg(not(target_arch = "wasm32"))]
98        {
99            req_builder = req_builder.version(parts.version);
100        }
101
102        // Don't set body if body is empty.
103        if !body.is_empty() {
104            #[cfg(not(target_arch = "wasm32"))]
105            {
106                req_builder = req_builder.body(reqwest::Body::wrap(HttpBufferBody(body)))
107            }
108            #[cfg(target_arch = "wasm32")]
109            {
110                req_builder = req_builder.body(reqwest::Body::from(body.to_bytes()))
111            }
112        }
113
114        let mut resp = req_builder.send().await.map_err(|err| {
115            Error::new(ErrorKind::Unexpected, "send http request")
116                .with_operation("reqwest::send")
117                .with_context("url", uri.to_string())
118                .with_temporary(is_temporary_error(&err))
119                .set_source(err)
120        })?;
121
122        // Get content length from header so that we can check it.
123        //
124        // - If the request method is HEAD, we will ignore content length.
125        // - If response contains content_encoding, we should omit its content length.
126        let content_length = if is_head || parse_content_encoding(resp.headers())?.is_some() {
127            None
128        } else {
129            parse_content_length(resp.headers())?
130        };
131
132        let mut hr = Response::builder()
133            .status(resp.status())
134            // Insert uri into response extension so that we can fetch
135            // it later.
136            .extension(uri.clone());
137
138        // Response builder under wasm doesn't support set version.
139        #[cfg(not(target_arch = "wasm32"))]
140        {
141            hr = hr.version(resp.version());
142        }
143
144        // Swap headers directly instead of copy the entire map.
145        mem::swap(hr.headers_mut().unwrap(), resp.headers_mut());
146
147        let bs = HttpBody::new(
148            resp.bytes_stream()
149                .try_filter(|v| future::ready(!v.is_empty()))
150                .map_ok(Buffer::from)
151                .map_err(move |err| {
152                    Error::new(ErrorKind::Unexpected, "read data from http response")
153                        .with_operation("reqwest::fetch")
154                        .with_context("url", uri.to_string())
155                        .with_temporary(is_temporary_error(&err))
156                        .set_source(err)
157                }),
158            content_length,
159        );
160
161        let resp = hr.body(bs).expect("response must build succeed");
162        Ok(resp)
163    }
164}
165
166/// Install the process-wide default reqwest transport.
167///
168/// The reqwest client is initialized when the transport handles its first
169/// request.
170#[doc(hidden)]
171pub fn install_default() {
172    HttpTransporter::install_default(LazyReqwestTransport);
173}
174
175struct LazyReqwestTransport;
176
177impl HttpTransport for LazyReqwestTransport {
178    async fn fetch(&self, req: Request<Buffer>) -> Result<Response<HttpBody>> {
179        DEFAULT_REQWEST_TRANSPORT.fetch(req).await
180    }
181}
182
183#[inline]
184fn is_temporary_error(err: &reqwest::Error) -> bool {
185    // error sending request
186    err.is_request()||
187    // request or response body error
188    err.is_body() ||
189    // error decoding response body, for example, connection reset.
190    err.is_decode()
191}
192
193#[cfg(not(target_arch = "wasm32"))]
194struct HttpBufferBody(Buffer);
195
196#[cfg(not(target_arch = "wasm32"))]
197impl http_body::Body for HttpBufferBody {
198    type Data = bytes::Bytes;
199    type Error = std::convert::Infallible;
200
201    fn poll_frame(
202        mut self: std::pin::Pin<&mut Self>,
203        _: &mut std::task::Context<'_>,
204    ) -> std::task::Poll<Option<Result<http_body::Frame<Self::Data>, Self::Error>>> {
205        match self.0.next() {
206            Some(bs) => std::task::Poll::Ready(Some(Ok(http_body::Frame::data(bs)))),
207            None => std::task::Poll::Ready(None),
208        }
209    }
210
211    fn is_end_stream(&self) -> bool {
212        self.0.is_empty()
213    }
214
215    fn size_hint(&self) -> http_body::SizeHint {
216        http_body::SizeHint::with_exact(self.0.len() as u64)
217    }
218}
219
220#[cfg(test)]
221mod tests {
222    use super::*;
223
224    #[test]
225    fn test_install_default_is_lazy() {
226        install_default();
227    }
228
229    #[cfg(any(feature = "rustls", feature = "native-tls"))]
230    #[test]
231    fn test_default_transport_succeeds() {
232        let transport = ReqwestTransport::default();
233        assert_eq!(format!("{:?}", transport), "ReqwestTransport");
234    }
235
236    #[test]
237    fn test_from_reqwest_client() {
238        let client = reqwest::Client::new();
239        let transport = ReqwestTransport::from(client);
240        assert_eq!(format!("{:?}", transport), "ReqwestTransport");
241    }
242}