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
196const ERROR_BODY_LIMIT: usize = 64 * 1024;
198
199async fn non_success_status_error(response: reqwest::Response) -> Error {
202 let status = response.status();
203 let headers = response.headers().clone();
204 non_success_body_error(status, headers, &mut Box::pin(response.bytes_stream())).await
205}
206
207pub async fn non_success_body_error<S, B, E>(
216 status: http::StatusCode,
217 headers: http::HeaderMap,
218 body: &mut S,
219) -> Error
220where
221 S: futures::Stream<Item = std::result::Result<B, E>> + Unpin,
222 B: AsRef<[u8]>,
223 E: std::fmt::Display,
224{
225 use futures::StreamExt;
226
227 let mut bytes = Vec::new();
228 let mut failure = None;
229 while let Some(chunk) = body.next().await {
230 match chunk {
231 Ok(chunk) => {
232 let remaining = ERROR_BODY_LIMIT + 1 - bytes.len();
234 bytes.extend(chunk.as_ref().iter().take(remaining));
235 if bytes.len() > ERROR_BODY_LIMIT {
236 break;
237 }
238 }
239 Err(error) => {
240 failure = Some(error.to_string());
241 break;
242 }
243 }
244 }
245 let kept = bytes.get(..ERROR_BODY_LIMIT).unwrap_or(&bytes);
246 let mut text = if kept.is_empty() {
247 failure
248 .map(|failure| format!("failed to read error response body: {failure}"))
249 .unwrap_or_default()
250 } else {
251 decode_error_body(kept.to_vec(), &headers).await
252 };
253 if bytes.len() > ERROR_BODY_LIMIT {
254 text.push_str("\n[Response body truncated after 64 KiB]");
255 }
256 Error::non_success_with_details(status, headers, text)
257}
258
259#[cfg(not(target_family = "wasm"))]
261async fn decode_error_body(bytes: Vec<u8>, headers: &http::HeaderMap) -> String {
262 let mut response = http::Response::new(bytes);
263 *response.headers_mut() = headers.clone();
264 reqwest::Response::from(response)
265 .text()
266 .await
267 .unwrap_or_else(|error| format!("failed to read error response body: {error}"))
268}
269
270#[cfg(target_family = "wasm")]
271async fn decode_error_body(bytes: Vec<u8>, _headers: &http::HeaderMap) -> String {
272 let bytes = bytes.strip_prefix(&[0xef, 0xbb, 0xbf]).unwrap_or(&bytes);
273 String::from_utf8_lossy(bytes).into_owned()
274}
275
276async fn into_response<U>(mut response: reqwest::Response) -> Result<Response<LazyBody<U>>>
278where
279 U: From<Bytes>,
280 U: WasmCompatSend + 'static,
281{
282 if !response.status().is_success() {
283 return Err(non_success_status_error(response).await);
284 }
285
286 let mut res = Response::builder().status(response.status());
287 if let Some(headers) = res.headers_mut() {
288 *headers = std::mem::take(response.headers_mut());
289 }
290
291 let body = async {
292 let bytes = response.bytes().await.map_err(Error::instance)?;
293 Ok(U::from(bytes))
294 };
295 #[cfg(not(target_family = "wasm"))]
296 let body = rig_http::runtime::bind(body)?;
297 let body: LazyBody<U> = Box::pin(body);
298 res.body(body).map_err(Error::Protocol)
299}
300
301fn streaming_head(response: &mut reqwest::Response) -> http::response::Builder {
302 #[cfg(not(target_family = "wasm"))]
303 let mut res = Response::builder()
304 .status(response.status())
305 .version(response.version());
306
307 #[cfg(target_family = "wasm")]
308 let mut res = Response::builder().status(response.status());
309
310 if let Some(hs) = res.headers_mut() {
311 *hs = std::mem::take(response.headers_mut());
312 }
313 res
314}
315
316async fn into_streaming_response(mut response: reqwest::Response) -> Result<StreamingResponse> {
320 if !response.status().is_success() {
321 return Err(non_success_status_error(response).await);
322 }
323 let res = streaming_head(&mut response);
324
325 use futures::StreamExt;
326 let stream = response
327 .bytes_stream()
328 .map(|chunk| chunk.map_err(Error::instance));
329 #[cfg(not(target_family = "wasm"))]
330 let stream = rig_http::runtime::bind_stream(stream)?;
331 let stream: Pin<Box<dyn WasmCompatSendStream<InnerItem = Result<Bytes>>>> = Box::pin(stream);
332 res.body(stream).map_err(Error::Protocol)
333}
334
335#[derive(Debug, thiserror::Error)]
337#[error("multipart part {part:?} has an unusable content type {content_type:?}: {source}")]
338struct InvalidPartContentType {
339 part: String,
340 content_type: String,
341 source: reqwest::Error,
342}
343
344pub fn multipart_form(value: MultipartForm) -> Result<reqwest::multipart::Form> {
348 let mut form = reqwest::multipart::Form::new();
349
350 for part in value.into_parts() {
351 let (name, content, filename, content_type) = part.into_pieces();
352 match content {
353 PartContent::Text(text) => {
354 form = form.text(name, text);
355 }
356 PartContent::Binary(bytes) => {
357 let mut req_part = reqwest::multipart::Part::stream(bytes);
359 if let Some(content_type) = content_type.as_ref() {
360 req_part = req_part.mime_str(content_type.as_ref()).map_err(|source| {
361 Error::instance(InvalidPartContentType {
362 part: name.clone(),
363 content_type: content_type.as_ref().to_string(),
364 source,
365 })
366 })?;
367 }
368
369 if let Some(filename) = filename {
370 req_part = req_part.file_name(filename);
371 }
372
373 form = form.part(name, req_part);
374 }
375 }
376 }
377
378 Ok(form)
379}
380
381enum Payload {
383 Bytes(Bytes),
384 Form(reqwest::multipart::Form),
385}
386
387trait Execute: WasmCompatSend + WasmCompatSync {
389 fn execute(
391 &self,
392 parts: http::request::Parts,
393 payload: Result<Payload>,
394 ) -> Result<impl Future<Output = Result<reqwest::Response>> + WasmCompatSend + 'static>;
395}
396
397impl Execute for ReqwestClient {
398 fn execute(
399 &self,
400 parts: http::request::Parts,
401 payload: Result<Payload>,
402 ) -> Result<impl Future<Output = Result<reqwest::Response>> + WasmCompatSend + 'static> {
403 let client = match &self.0 {
404 Built::Client(client) => client,
405 Built::Failed(error) => return Err(unbuilt(error)),
406 };
407 let request = client
408 .request(parts.method, parts.uri.to_string())
409 .headers(parts.headers);
410 let request = match payload? {
411 Payload::Bytes(bytes) => request.body(bytes),
412 Payload::Form(form) => request.multipart(form),
413 };
414 Ok(async move { request.send().await.map_err(Error::instance) })
415 }
416}
417
418fn send_with<C, T, F>(
422 client: &C,
423 parts: http::request::Parts,
424 payload: Result<Payload>,
425 convert: impl FnOnce(reqwest::Response) -> F + WasmCompatSend + 'static,
426) -> impl Future<Output = Result<T>> + WasmCompatSend + 'static
427where
428 C: Execute,
429 F: Future<Output = Result<T>> + WasmCompatSend,
430{
431 let sent = client.execute(parts, payload);
432 async move {
433 let sent = sent?;
434 let operation = async move { convert(sent.await?).await };
435 #[cfg(not(target_family = "wasm"))]
436 let operation = rig_http::runtime::bind(operation)?;
437 operation.await
438 }
439}
440
441macro_rules! http_client_ext {
443 ($client:ty) => {
444 impl HttpClientExt for $client {
445 fn send<T, U>(
446 &self,
447 req: Request<T>,
448 ) -> impl Future<Output = Result<Response<LazyBody<U>>>> + WasmCompatSend + 'static
449 where
450 T: Into<Bytes>,
451 U: From<Bytes> + WasmCompatSend + 'static,
452 {
453 let (parts, body) = req.into_parts();
454 let payload = Ok(Payload::Bytes(body.into()));
455 send_with(self, parts, payload, into_response::<U>)
456 }
457
458 fn send_multipart<U>(
459 &self,
460 req: Request<MultipartForm>,
461 ) -> impl Future<Output = Result<Response<LazyBody<U>>>> + WasmCompatSend + 'static
462 where
463 U: From<Bytes> + WasmCompatSend + 'static,
464 {
465 let (parts, form) = req.into_parts();
466 let payload = multipart_form(form).map(Payload::Form);
468 send_with(self, parts, payload, into_response::<U>)
469 }
470
471 fn send_streaming<T>(
472 &self,
473 req: Request<T>,
474 ) -> impl Future<Output = Result<StreamingResponse>> + WasmCompatSend
475 where
476 T: Into<Bytes> + WasmCompatSend,
477 {
478 let (parts, body) = req.into_parts();
479 let payload = Ok(Payload::Bytes(body.into()));
480 send_with(self, parts, payload, into_streaming_response)
481 }
482 }
483 };
484}
485http_client_ext!(ReqwestClient);
486
487#[cfg(any(
488 feature = "reqwest-middleware-rustls",
489 feature = "reqwest-middleware-native-tls"
490))]
491mod middleware;
492#[cfg(any(
493 feature = "reqwest-middleware-rustls",
494 feature = "reqwest-middleware-native-tls"
495))]
496pub use middleware::ReqwestMiddlewareClient;
497
498#[cfg(not(target_family = "wasm"))]
501const _: fn() = || {
502 fn assert_send_sync_static<T: Send + Sync + 'static>() {}
503 assert_send_sync_static::<ReqwestClient>();
504};
505
506#[cfg(test)]
507mod tests;