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::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#[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 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 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 #[cfg(not(target_arch = "wasm32"))]
98 {
99 req_builder = req_builder.version(parts.version);
100 }
101
102 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 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 .extension(uri.clone());
137
138 #[cfg(not(target_arch = "wasm32"))]
140 {
141 hr = hr.version(resp.version());
142 }
143
144 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#[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 err.is_request()||
187 err.is_body() ||
189 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}