1#![cfg_attr(docsrs, feature(doc_cfg))]
2#![cfg_attr(
3 test,
4 allow(
5 clippy::expect_used,
6 clippy::indexing_slicing,
7 clippy::panic,
8 clippy::unwrap_used,
9 clippy::unreachable
10 )
11)]
12pub use reqwest;
36
37#[derive(Clone, Debug)]
45pub struct ReqwestClient(Built);
46
47pub fn shared() -> DynHttpClient {
49 DynHttpClient::new(ReqwestClient::default())
50}
51
52#[derive(Clone, Debug)]
54enum Built {
55 Client(Arc<reqwest::Client>),
56 Failed(Arc<reqwest::Error>),
60}
61
62impl Default for ReqwestClient {
63 fn default() -> Self {
66 fn build() -> ReqwestClient {
67 let built = reqwest::Client::builder().build();
68 #[cfg(all(feature = "rustls", not(target_family = "wasm")))]
69 let built = built.or_else(with_bundled_roots);
70 ReqwestClient(match built {
71 Ok(client) => Built::Client(Arc::new(client)),
72 Err(error) => Built::Failed(Arc::new(error)),
73 })
74 }
75 #[cfg(not(target_family = "wasm"))]
76 {
77 static SHARED: std::sync::LazyLock<ReqwestClient> = std::sync::LazyLock::new(build);
78 SHARED.clone()
79 }
80 #[cfg(target_family = "wasm")]
81 {
82 thread_local! {
83 static SHARED: ReqwestClient = build();
84 }
85 SHARED.with(Clone::clone)
86 }
87 }
88}
89
90impl ReqwestClient {
91 pub fn inner(&self) -> Option<&reqwest::Client> {
94 match &self.0 {
95 Built::Client(client) => Some(client),
96 Built::Failed(_) => None,
97 }
98 }
99
100 #[cfg(test)]
102 fn same(&self, other: &Self) -> bool {
103 match (&self.0, &other.0) {
104 (Built::Client(a), Built::Client(b)) => Arc::ptr_eq(a, b),
105 (Built::Failed(a), Built::Failed(b)) => Arc::ptr_eq(a, b),
106 _ => false,
107 }
108 }
109}
110
111#[cfg(all(feature = "rustls", not(target_family = "wasm")))]
116fn with_bundled_roots(error: reqwest::Error) -> reqwest::Result<reqwest::Client> {
117 let roots = webpki_root_certs::TLS_SERVER_ROOT_CERTS
118 .iter()
119 .filter_map(|der| reqwest::Certificate::from_der(der).ok());
120 let Ok(client) = reqwest::Client::builder().tls_certs_only(roots).build() else {
121 return Err(error);
122 };
123 tracing::warn!(
124 "the system's CA certificates could not be loaded ({}), so rig trusts its bundled \
125 Mozilla roots; install the system's CA certificates, or set SSL_CERT_FILE or \
126 SSL_CERT_DIR, to trust those instead",
127 Chain(&error)
128 );
129 Ok(client)
130}
131
132fn unbuilt(error: &Arc<reqwest::Error>) -> Error {
134 Error::unbuilt(TransportBuildError(Arc::clone(error)))
135}
136
137impl From<reqwest::Client> for ReqwestClient {
138 fn from(client: reqwest::Client) -> Self {
139 Self(Built::Client(Arc::new(client)))
140 }
141}
142
143use bytes::Bytes;
144use rig_http::http_client::{
145 DynHttpClient, Error, HttpClientExt, LazyBody, MultipartForm, Request, Response, Result,
146 StreamingResponse, multipart::PartContent,
147};
148use rig_http::wasm_compat::*;
149use std::pin::Pin;
150use std::sync::Arc;
151
152#[derive(Debug)]
155struct TransportBuildError(Arc<reqwest::Error>);
156
157impl std::fmt::Display for TransportBuildError {
158 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
159 write!(
160 f,
161 "could not build the bundled reqwest transport: {}",
162 Chain(&*self.0)
163 )
164 }
165}
166
167struct Chain<'a>(&'a (dyn std::error::Error + 'static));
169
170impl std::fmt::Display for Chain<'_> {
171 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
172 write!(f, "{}", self.0)?;
173 let mut source = self.0.source();
174 while let Some(cause) = source {
175 write!(f, ": {cause}")?;
176 source = cause.source();
177 }
178 Ok(())
179 }
180}
181
182impl std::error::Error for TransportBuildError {
183 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
184 Some(&*self.0)
185 }
186}
187
188pub fn from_reqwest(err: reqwest::Error) -> Error {
193 Error::instance(err)
194}
195
196async fn non_success_status_error(response: reqwest::Response) -> Error {
200 use futures::StreamExt;
201
202 const LIMIT: usize = 64 * 1024;
203 let status = response.status();
204 let headers = response.headers().clone();
205 let mut stream = Box::pin(response.bytes_stream());
206 let mut bytes = Vec::new();
207 let mut truncated = false;
208 let mut failure = None;
209 while let Some(chunk) = stream.next().await {
210 match chunk {
211 Ok(chunk) => {
212 let remaining = LIMIT - bytes.len();
213 bytes.extend(chunk.iter().take(remaining));
214 if chunk.len() > remaining {
215 truncated = true;
216 break;
217 }
218 }
219 Err(error) => {
220 failure = Some(error);
221 break;
222 }
223 }
224 }
225 drop(stream);
226 let mut body = if bytes.is_empty() {
227 failure
228 .map(|error| format!("failed to read error response body: {error}"))
229 .unwrap_or_default()
230 } else {
231 decode_error_body(bytes, &headers).await
232 };
233 if truncated {
234 body.push_str("\n[Response body truncated after 64 KiB]");
235 }
236 Error::non_success_with_details(status, headers, body)
237}
238
239#[cfg(not(target_family = "wasm"))]
241async fn decode_error_body(bytes: Vec<u8>, headers: &http::HeaderMap) -> String {
242 let mut response = http::Response::new(bytes);
243 *response.headers_mut() = headers.clone();
244 reqwest::Response::from(response)
245 .text()
246 .await
247 .unwrap_or_else(|error| format!("failed to read error response body: {error}"))
248}
249
250#[cfg(target_family = "wasm")]
251async fn decode_error_body(bytes: Vec<u8>, _headers: &http::HeaderMap) -> String {
252 let bytes = bytes.strip_prefix(&[0xef, 0xbb, 0xbf]).unwrap_or(&bytes);
253 String::from_utf8_lossy(bytes).into_owned()
254}
255
256async fn into_response<U>(mut response: reqwest::Response) -> Result<Response<LazyBody<U>>>
258where
259 U: From<Bytes>,
260 U: WasmCompatSend + 'static,
261{
262 if !response.status().is_success() {
263 return Err(non_success_status_error(response).await);
264 }
265
266 let mut res = Response::builder().status(response.status());
267 if let Some(headers) = res.headers_mut() {
268 *headers = std::mem::take(response.headers_mut());
269 }
270
271 let body = async {
272 let bytes = response.bytes().await.map_err(Error::instance)?;
273 Ok(U::from(bytes))
274 };
275 #[cfg(not(target_family = "wasm"))]
276 let body = rig_http::runtime::bind(body)?;
277 let body: LazyBody<U> = Box::pin(body);
278 res.body(body).map_err(Error::Protocol)
279}
280
281fn streaming_head(response: &mut reqwest::Response) -> http::response::Builder {
282 #[cfg(not(target_family = "wasm"))]
283 let mut res = Response::builder()
284 .status(response.status())
285 .version(response.version());
286
287 #[cfg(target_family = "wasm")]
288 let mut res = Response::builder().status(response.status());
289
290 if let Some(hs) = res.headers_mut() {
291 *hs = std::mem::take(response.headers_mut());
292 }
293 res
294}
295
296async fn into_streaming_response(mut response: reqwest::Response) -> Result<StreamingResponse> {
300 if !response.status().is_success() {
301 return Err(non_success_status_error(response).await);
302 }
303 let res = streaming_head(&mut response);
304
305 use futures::StreamExt;
306 let stream = response
307 .bytes_stream()
308 .map(|chunk| chunk.map_err(Error::instance));
309 #[cfg(not(target_family = "wasm"))]
310 let stream = rig_http::runtime::bind_stream(stream)?;
311 let stream: Pin<Box<dyn WasmCompatSendStream<InnerItem = Result<Bytes>>>> = Box::pin(stream);
312 res.body(stream).map_err(Error::Protocol)
313}
314
315#[derive(Debug, thiserror::Error)]
317#[error("multipart part {part:?} has an unusable content type {content_type:?}: {source}")]
318struct InvalidPartContentType {
319 part: String,
320 content_type: String,
321 source: reqwest::Error,
322}
323
324pub fn multipart_form(value: MultipartForm) -> Result<reqwest::multipart::Form> {
328 let mut form = reqwest::multipart::Form::new();
329
330 for part in value.into_parts() {
331 let (name, content, filename, content_type) = part.into_pieces();
332 match content {
333 PartContent::Text(text) => {
334 form = form.text(name, text);
335 }
336 PartContent::Binary(bytes) => {
337 let mut req_part = reqwest::multipart::Part::stream(bytes);
339 if let Some(content_type) = content_type.as_ref() {
340 req_part = req_part.mime_str(content_type.as_ref()).map_err(|source| {
341 Error::instance(InvalidPartContentType {
342 part: name.clone(),
343 content_type: content_type.as_ref().to_string(),
344 source,
345 })
346 })?;
347 }
348
349 if let Some(filename) = filename {
350 req_part = req_part.file_name(filename);
351 }
352
353 form = form.part(name, req_part);
354 }
355 }
356 }
357
358 Ok(form)
359}
360
361enum Payload {
363 Bytes(Bytes),
364 Form(reqwest::multipart::Form),
365}
366
367trait Execute: WasmCompatSend + WasmCompatSync {
369 fn execute(
371 &self,
372 parts: http::request::Parts,
373 payload: Result<Payload>,
374 ) -> Result<impl Future<Output = Result<reqwest::Response>> + WasmCompatSend + 'static>;
375}
376
377impl Execute for ReqwestClient {
378 fn execute(
379 &self,
380 parts: http::request::Parts,
381 payload: Result<Payload>,
382 ) -> Result<impl Future<Output = Result<reqwest::Response>> + WasmCompatSend + 'static> {
383 let client = match &self.0 {
384 Built::Client(client) => client,
385 Built::Failed(error) => return Err(unbuilt(error)),
386 };
387 let request = client
388 .request(parts.method, parts.uri.to_string())
389 .headers(parts.headers);
390 let request = match payload? {
391 Payload::Bytes(bytes) => request.body(bytes),
392 Payload::Form(form) => request.multipart(form),
393 };
394 Ok(async move { request.send().await.map_err(Error::instance) })
395 }
396}
397
398fn send_with<C, T, F>(
402 client: &C,
403 parts: http::request::Parts,
404 payload: Result<Payload>,
405 convert: impl FnOnce(reqwest::Response) -> F + WasmCompatSend + 'static,
406) -> impl Future<Output = Result<T>> + WasmCompatSend + 'static
407where
408 C: Execute,
409 F: Future<Output = Result<T>> + WasmCompatSend,
410{
411 let sent = client.execute(parts, payload);
412 async move {
413 let sent = sent?;
414 let operation = async move { convert(sent.await?).await };
415 #[cfg(not(target_family = "wasm"))]
416 let operation = rig_http::runtime::bind(operation)?;
417 operation.await
418 }
419}
420
421macro_rules! http_client_ext {
423 ($client:ty) => {
424 impl HttpClientExt for $client {
425 fn send<T, U>(
426 &self,
427 req: Request<T>,
428 ) -> impl Future<Output = Result<Response<LazyBody<U>>>> + WasmCompatSend + 'static
429 where
430 T: Into<Bytes>,
431 U: From<Bytes> + WasmCompatSend + 'static,
432 {
433 let (parts, body) = req.into_parts();
434 let payload = Ok(Payload::Bytes(body.into()));
435 send_with(self, parts, payload, into_response::<U>)
436 }
437
438 fn send_multipart<U>(
439 &self,
440 req: Request<MultipartForm>,
441 ) -> impl Future<Output = Result<Response<LazyBody<U>>>> + WasmCompatSend + 'static
442 where
443 U: From<Bytes> + WasmCompatSend + 'static,
444 {
445 let (parts, form) = req.into_parts();
446 let payload = multipart_form(form).map(Payload::Form);
448 send_with(self, parts, payload, into_response::<U>)
449 }
450
451 fn send_streaming<T>(
452 &self,
453 req: Request<T>,
454 ) -> impl Future<Output = Result<StreamingResponse>> + WasmCompatSend
455 where
456 T: Into<Bytes> + WasmCompatSend,
457 {
458 let (parts, body) = req.into_parts();
459 let payload = Ok(Payload::Bytes(body.into()));
460 send_with(self, parts, payload, into_streaming_response)
461 }
462 }
463 };
464}
465http_client_ext!(ReqwestClient);
466
467#[cfg(any(
468 feature = "reqwest-middleware-rustls",
469 feature = "reqwest-middleware-native-tls"
470))]
471mod middleware;
472#[cfg(any(
473 feature = "reqwest-middleware-rustls",
474 feature = "reqwest-middleware-native-tls"
475))]
476pub use middleware::ReqwestMiddlewareClient;
477
478#[cfg(not(target_family = "wasm"))]
481const _: fn() = || {
482 fn assert_send_sync_static<T: Send + Sync + 'static>() {}
483 assert_send_sync_static::<ReqwestClient>();
484};
485
486#[cfg(test)]
487mod tests;