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)]
43pub struct ReqwestClient(Built);
44
45pub fn shared() -> DynHttpClient {
47 DynHttpClient::new(ReqwestClient::default())
48}
49
50#[derive(Clone, Debug)]
52enum Built {
53 Client(Arc<reqwest::Client>),
54 Failed(Arc<reqwest::Error>),
58}
59
60impl Default for ReqwestClient {
61 fn default() -> Self {
64 fn build() -> ReqwestClient {
65 let built = reqwest::Client::builder().build();
66 #[cfg(all(feature = "rustls", not(target_family = "wasm")))]
67 let built = built.or_else(with_bundled_roots);
68 ReqwestClient(match built {
69 Ok(client) => Built::Client(Arc::new(client)),
70 Err(error) => Built::Failed(Arc::new(error)),
71 })
72 }
73 #[cfg(not(target_family = "wasm"))]
74 {
75 static SHARED: std::sync::LazyLock<ReqwestClient> = std::sync::LazyLock::new(build);
76 SHARED.clone()
77 }
78 #[cfg(target_family = "wasm")]
79 {
80 thread_local! {
81 static SHARED: ReqwestClient = build();
82 }
83 SHARED.with(Clone::clone)
84 }
85 }
86}
87
88impl ReqwestClient {
89 pub fn inner(&self) -> Option<&reqwest::Client> {
92 match &self.0 {
93 Built::Client(client) => Some(client),
94 Built::Failed(_) => None,
95 }
96 }
97
98 #[cfg(test)]
100 fn same(&self, other: &Self) -> bool {
101 match (&self.0, &other.0) {
102 (Built::Client(a), Built::Client(b)) => Arc::ptr_eq(a, b),
103 (Built::Failed(a), Built::Failed(b)) => Arc::ptr_eq(a, b),
104 _ => false,
105 }
106 }
107}
108
109#[cfg(all(feature = "rustls", not(target_family = "wasm")))]
114fn with_bundled_roots(error: reqwest::Error) -> reqwest::Result<reqwest::Client> {
115 let roots = webpki_root_certs::TLS_SERVER_ROOT_CERTS
116 .iter()
117 .filter_map(|der| reqwest::Certificate::from_der(der).ok());
118 let Ok(client) = reqwest::Client::builder().tls_certs_only(roots).build() else {
119 return Err(error);
120 };
121 tracing::warn!(
122 "the system's CA certificates could not be loaded ({}), so rig trusts its bundled \
123 Mozilla roots; install the system's CA certificates, or set SSL_CERT_FILE or \
124 SSL_CERT_DIR, to trust those instead",
125 Chain(&error)
126 );
127 Ok(client)
128}
129
130fn unbuilt(error: &Arc<reqwest::Error>) -> Error {
132 Error::unbuilt(TransportBuildError(Arc::clone(error)))
133}
134
135impl From<reqwest::Client> for ReqwestClient {
136 fn from(client: reqwest::Client) -> Self {
137 Self(Built::Client(Arc::new(client)))
138 }
139}
140
141use bytes::Bytes;
142use rig_http::http_client::{
143 DynHttpClient, Error, HttpClientExt, LazyBody, MultipartForm, Request, Response, Result,
144 StreamingResponse, multipart::PartContent,
145};
146use rig_http::wasm_compat::*;
147use std::pin::Pin;
148use std::sync::Arc;
149
150#[derive(Debug)]
153struct TransportBuildError(Arc<reqwest::Error>);
154
155impl std::fmt::Display for TransportBuildError {
156 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
157 write!(
158 f,
159 "could not build the bundled reqwest transport: {}",
160 Chain(&*self.0)
161 )
162 }
163}
164
165struct Chain<'a>(&'a (dyn std::error::Error + 'static));
167
168impl std::fmt::Display for Chain<'_> {
169 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
170 write!(f, "{}", self.0)?;
171 let mut source = self.0.source();
172 while let Some(cause) = source {
173 write!(f, ": {cause}")?;
174 source = cause.source();
175 }
176 Ok(())
177 }
178}
179
180impl std::error::Error for TransportBuildError {
181 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
182 Some(&*self.0)
183 }
184}
185
186pub fn from_reqwest(err: reqwest::Error) -> Error {
191 Error::instance(err)
192}
193
194async fn non_success_status_error(response: reqwest::Response) -> Error {
197 let status = response.status();
198 let headers = response.headers().clone();
199 let body = response
200 .text()
201 .await
202 .unwrap_or_else(|error| format!("failed to read error response body: {error}"));
203 Error::non_success_with_details(status, headers, body)
204}
205
206async fn into_response<U>(mut response: reqwest::Response) -> Result<Response<LazyBody<U>>>
208where
209 U: From<Bytes>,
210 U: WasmCompatSend + 'static,
211{
212 if !response.status().is_success() {
213 return Err(non_success_status_error(response).await);
214 }
215
216 let mut res = Response::builder().status(response.status());
217 if let Some(headers) = res.headers_mut() {
218 *headers = std::mem::take(response.headers_mut());
219 }
220
221 let body = async {
222 let bytes = response.bytes().await.map_err(Error::instance)?;
223 Ok(U::from(bytes))
224 };
225 #[cfg(not(target_family = "wasm"))]
226 let body = rig_http::runtime::bind(body)?;
227 let body: LazyBody<U> = Box::pin(body);
228 res.body(body).map_err(Error::Protocol)
229}
230
231fn streaming_head(response: &mut reqwest::Response) -> http::response::Builder {
232 #[cfg(not(target_family = "wasm"))]
233 let mut res = Response::builder()
234 .status(response.status())
235 .version(response.version());
236
237 #[cfg(target_family = "wasm")]
238 let mut res = Response::builder().status(response.status());
239
240 if let Some(hs) = res.headers_mut() {
241 *hs = std::mem::take(response.headers_mut());
242 }
243 res
244}
245
246async fn into_streaming_response(mut response: reqwest::Response) -> Result<StreamingResponse> {
250 if !response.status().is_success() {
251 return Err(non_success_status_error(response).await);
252 }
253 let res = streaming_head(&mut response);
254
255 use futures::StreamExt;
256 let stream = response
257 .bytes_stream()
258 .map(|chunk| chunk.map_err(Error::instance));
259 #[cfg(not(target_family = "wasm"))]
260 let stream = rig_http::runtime::bind_stream(stream)?;
261 let stream: Pin<Box<dyn WasmCompatSendStream<InnerItem = Result<Bytes>>>> = Box::pin(stream);
262 res.body(stream).map_err(Error::Protocol)
263}
264
265#[derive(Debug, thiserror::Error)]
267#[error("multipart part {part:?} has an unusable content type {content_type:?}: {source}")]
268struct InvalidPartContentType {
269 part: String,
270 content_type: String,
271 source: reqwest::Error,
272}
273
274pub fn multipart_form(value: MultipartForm) -> Result<reqwest::multipart::Form> {
278 let mut form = reqwest::multipart::Form::new();
279
280 for part in value.into_parts() {
281 let (name, content, filename, content_type) = part.into_pieces();
282 match content {
283 PartContent::Text(text) => {
284 form = form.text(name, text);
285 }
286 PartContent::Binary(bytes) => {
287 let mut req_part = reqwest::multipart::Part::stream(bytes);
289 if let Some(content_type) = content_type.as_ref() {
290 req_part = req_part.mime_str(content_type.as_ref()).map_err(|source| {
291 Error::instance(InvalidPartContentType {
292 part: name.clone(),
293 content_type: content_type.as_ref().to_string(),
294 source,
295 })
296 })?;
297 }
298
299 if let Some(filename) = filename {
300 req_part = req_part.file_name(filename);
301 }
302
303 form = form.part(name, req_part);
304 }
305 }
306 }
307
308 Ok(form)
309}
310
311enum Payload {
313 Bytes(Bytes),
314 Form(reqwest::multipart::Form),
315}
316
317trait Execute: WasmCompatSend + WasmCompatSync {
319 fn execute(
321 &self,
322 parts: http::request::Parts,
323 payload: Result<Payload>,
324 ) -> Result<impl Future<Output = Result<reqwest::Response>> + WasmCompatSend + 'static>;
325}
326
327impl Execute for ReqwestClient {
328 fn execute(
329 &self,
330 parts: http::request::Parts,
331 payload: Result<Payload>,
332 ) -> Result<impl Future<Output = Result<reqwest::Response>> + WasmCompatSend + 'static> {
333 let client = match &self.0 {
334 Built::Client(client) => client,
335 Built::Failed(error) => return Err(unbuilt(error)),
336 };
337 let request = client
338 .request(parts.method, parts.uri.to_string())
339 .headers(parts.headers);
340 let request = match payload? {
341 Payload::Bytes(bytes) => request.body(bytes),
342 Payload::Form(form) => request.multipart(form),
343 };
344 Ok(async move { request.send().await.map_err(Error::instance) })
345 }
346}
347
348fn send_with<C, T, F>(
352 client: &C,
353 parts: http::request::Parts,
354 payload: Result<Payload>,
355 convert: impl FnOnce(reqwest::Response) -> F + WasmCompatSend + 'static,
356) -> impl Future<Output = Result<T>> + WasmCompatSend + 'static
357where
358 C: Execute,
359 F: Future<Output = Result<T>> + WasmCompatSend,
360{
361 let sent = client.execute(parts, payload);
362 async move {
363 let sent = sent?;
364 let operation = async move { convert(sent.await?).await };
365 #[cfg(not(target_family = "wasm"))]
366 let operation = rig_http::runtime::bind(operation)?;
367 operation.await
368 }
369}
370
371macro_rules! http_client_ext {
373 ($client:ty) => {
374 impl HttpClientExt for $client {
375 fn send<T, U>(
376 &self,
377 req: Request<T>,
378 ) -> impl Future<Output = Result<Response<LazyBody<U>>>> + WasmCompatSend + 'static
379 where
380 T: Into<Bytes>,
381 U: From<Bytes> + WasmCompatSend + 'static,
382 {
383 let (parts, body) = req.into_parts();
384 let payload = Ok(Payload::Bytes(body.into()));
385 send_with(self, parts, payload, into_response::<U>)
386 }
387
388 fn send_multipart<U>(
389 &self,
390 req: Request<MultipartForm>,
391 ) -> impl Future<Output = Result<Response<LazyBody<U>>>> + WasmCompatSend + 'static
392 where
393 U: From<Bytes> + WasmCompatSend + 'static,
394 {
395 let (parts, form) = req.into_parts();
396 let payload = multipart_form(form).map(Payload::Form);
398 send_with(self, parts, payload, into_response::<U>)
399 }
400
401 fn send_streaming<T>(
402 &self,
403 req: Request<T>,
404 ) -> impl Future<Output = Result<StreamingResponse>> + WasmCompatSend
405 where
406 T: Into<Bytes> + WasmCompatSend,
407 {
408 let (parts, body) = req.into_parts();
409 let payload = Ok(Payload::Bytes(body.into()));
410 send_with(self, parts, payload, into_streaming_response)
411 }
412 }
413 };
414}
415http_client_ext!(ReqwestClient);
416
417#[cfg(any(
418 feature = "reqwest-middleware-rustls",
419 feature = "reqwest-middleware-native-tls"
420))]
421mod middleware;
422#[cfg(any(
423 feature = "reqwest-middleware-rustls",
424 feature = "reqwest-middleware-native-tls"
425))]
426pub use middleware::ReqwestMiddlewareClient;
427
428#[cfg(not(target_family = "wasm"))]
431const _: fn() = || {
432 fn assert_send_sync_static<T: Send + Sync + 'static>() {}
433 assert_send_sync_static::<ReqwestClient>();
434};
435
436#[cfg(test)]
437mod tests;