opendal_http_transport_reqwest/
lib.rs1#![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::HttpRedirect;
36use opendal_core::HttpTransport;
37use opendal_core::HttpTransporter;
38use opendal_core::Result;
39use opendal_core::raw::parse_content_encoding;
40use opendal_core::raw::parse_content_length;
41
42static DEFAULT_REQWEST_TRANSPORT: LazyLock<ReqwestTransport> =
43 LazyLock::new(|| ReqwestTransport::new(reqwest::Client::new()));
44
45#[derive(Clone)]
47pub struct ReqwestTransport {
48 client: reqwest::Client,
49}
50
51impl Debug for ReqwestTransport {
52 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
53 f.debug_struct("ReqwestTransport").finish()
54 }
55}
56
57impl Default for ReqwestTransport {
58 fn default() -> Self {
59 DEFAULT_REQWEST_TRANSPORT.clone()
60 }
61}
62
63impl From<reqwest::Client> for ReqwestTransport {
64 fn from(client: reqwest::Client) -> Self {
65 Self::new(client)
66 }
67}
68
69impl ReqwestTransport {
70 pub fn new(client: reqwest::Client) -> Self {
72 Self { client }
73 }
74}
75
76impl HttpTransport for ReqwestTransport {
77 async fn fetch(&self, req: Request<Buffer>) -> Result<Response<HttpBody>> {
78 let uri = req.uri().clone();
81 let is_head = req.method() == http::Method::HEAD;
82
83 let (mut parts, body) = req.into_parts();
84 let original = uri.to_string();
85 let target = parts
86 .extensions
87 .get::<HttpRedirect>()
88 .map(|redirect| redirect.uri().original_uri())
89 .unwrap_or(&original);
90
91 let url = reqwest::Url::parse(target).map_err(|err| {
92 Error::new(ErrorKind::Unexpected, "request url is invalid")
93 .with_operation("reqwest::fetch")
94 .with_context("url", uri.to_string())
95 .set_source(err)
96 })?;
97
98 if parts.extensions.get::<HttpRedirect>().is_some() {
99 let original = reqwest::Url::parse(&uri.to_string()).map_err(|err| {
100 Error::new(ErrorKind::Unexpected, "original request url is invalid").set_source(err)
101 })?;
102 if original.origin() != url.origin() {
103 for name in [
106 "authorization",
107 "proxy-authorization",
108 "cookie",
109 "cookie2",
110 "www-authenticate",
111 ] {
112 parts.headers.insert(
113 http::header::HeaderName::from_static(name),
114 http::HeaderValue::from_static(""),
115 );
116 }
117 }
118 }
119
120 let mut req_builder = self
121 .client
122 .request(parts.method, url)
123 .headers(parts.headers);
124
125 #[cfg(not(target_arch = "wasm32"))]
127 {
128 req_builder = req_builder.version(parts.version);
129 }
130
131 if !body.is_empty() {
133 #[cfg(not(target_arch = "wasm32"))]
134 {
135 req_builder = req_builder.body(reqwest::Body::wrap(HttpBufferBody(body)))
136 }
137 #[cfg(target_arch = "wasm32")]
138 {
139 req_builder = req_builder.body(reqwest::Body::from(body.to_bytes()))
140 }
141 }
142
143 let mut resp = req_builder.send().await.map_err(|err| {
144 Error::new(ErrorKind::Unexpected, "send http request")
145 .with_operation("reqwest::send")
146 .with_context("url", uri.to_string())
147 .with_temporary(is_temporary_error(&err))
148 .set_source(err.without_url())
149 })?;
150
151 let content_length = if is_head || parse_content_encoding(resp.headers())?.is_some() {
156 None
157 } else {
158 parse_content_length(resp.headers())?
159 };
160
161 let mut hr = Response::builder()
162 .status(resp.status())
163 .extension(uri.clone());
166
167 if let Ok(target) = resp.url().as_str().parse::<http::Uri>()
169 && target != uri
170 {
171 let redirect = parts
172 .extensions
173 .get::<HttpRedirect>()
174 .filter(|redirect| redirect.uri().original_uri() == resp.url().as_str())
175 .cloned()
176 .unwrap_or_else(|| HttpRedirect::new(target));
177 hr = hr.extension(redirect);
178 }
179
180 #[cfg(not(target_arch = "wasm32"))]
182 {
183 hr = hr.version(resp.version());
184 }
185
186 mem::swap(hr.headers_mut().unwrap(), resp.headers_mut());
188
189 let bs = HttpBody::new(
190 resp.bytes_stream()
191 .try_filter(|v| future::ready(!v.is_empty()))
192 .map_ok(Buffer::from)
193 .map_err(move |err| {
194 Error::new(ErrorKind::Unexpected, "read data from http response")
195 .with_operation("reqwest::fetch")
196 .with_context("url", uri.to_string())
197 .with_temporary(is_temporary_error(&err))
198 .set_source(err.without_url())
199 }),
200 content_length,
201 );
202
203 let resp = hr.body(bs).expect("response must build succeed");
204 Ok(resp)
205 }
206}
207
208#[doc(hidden)]
213pub fn install_default() {
214 HttpTransporter::install_default(LazyReqwestTransport);
215}
216
217struct LazyReqwestTransport;
218
219impl HttpTransport for LazyReqwestTransport {
220 async fn fetch(&self, req: Request<Buffer>) -> Result<Response<HttpBody>> {
221 DEFAULT_REQWEST_TRANSPORT.fetch(req).await
222 }
223}
224
225#[inline]
226fn is_temporary_error(err: &reqwest::Error) -> bool {
227 err.is_request()||
229 err.is_body() ||
231 err.is_decode()
233}
234
235#[cfg(not(target_arch = "wasm32"))]
236struct HttpBufferBody(Buffer);
237
238#[cfg(not(target_arch = "wasm32"))]
239impl http_body::Body for HttpBufferBody {
240 type Data = bytes::Bytes;
241 type Error = std::convert::Infallible;
242
243 fn poll_frame(
244 mut self: std::pin::Pin<&mut Self>,
245 _: &mut std::task::Context<'_>,
246 ) -> std::task::Poll<Option<Result<http_body::Frame<Self::Data>, Self::Error>>> {
247 match self.0.next() {
248 Some(bs) => std::task::Poll::Ready(Some(Ok(http_body::Frame::data(bs)))),
249 None => std::task::Poll::Ready(None),
250 }
251 }
252
253 fn is_end_stream(&self) -> bool {
254 self.0.is_empty()
255 }
256
257 fn size_hint(&self) -> http_body::SizeHint {
258 http_body::SizeHint::with_exact(self.0.len() as u64)
259 }
260}
261
262#[cfg(test)]
263mod tests {
264 use super::*;
265
266 #[test]
267 fn test_install_default_is_lazy() {
268 install_default();
269 }
270
271 #[cfg(any(feature = "rustls", feature = "native-tls"))]
272 #[test]
273 fn test_default_transport_succeeds() {
274 let transport = ReqwestTransport::default();
275 assert_eq!(format!("{:?}", transport), "ReqwestTransport");
276 }
277
278 #[test]
279 fn test_from_reqwest_client() {
280 let client = reqwest::Client::new();
281 let transport = ReqwestTransport::from(client);
282 assert_eq!(format!("{:?}", transport), "ReqwestTransport");
283 }
284}