#![cfg_attr(docsrs, feature(doc_cfg))]
#![cfg_attr(
test,
allow(
clippy::expect_used,
clippy::indexing_slicing,
clippy::panic,
clippy::unwrap_used,
clippy::unreachable
)
)]
pub use reqwest;
#[derive(Clone, Debug)]
pub struct ReqwestClient(Built);
pub fn shared() -> DynHttpClient {
DynHttpClient::new(ReqwestClient::default())
}
#[derive(Clone, Debug)]
enum Built {
Client(Arc<reqwest::Client>),
Failed(Arc<reqwest::Error>),
}
impl Default for ReqwestClient {
fn default() -> Self {
fn build() -> ReqwestClient {
let built = reqwest::Client::builder().build();
#[cfg(all(feature = "rustls", not(target_family = "wasm")))]
let built = built.or_else(with_bundled_roots);
ReqwestClient(match built {
Ok(client) => Built::Client(Arc::new(client)),
Err(error) => Built::Failed(Arc::new(error)),
})
}
#[cfg(not(target_family = "wasm"))]
{
static SHARED: std::sync::LazyLock<ReqwestClient> = std::sync::LazyLock::new(build);
SHARED.clone()
}
#[cfg(target_family = "wasm")]
{
thread_local! {
static SHARED: ReqwestClient = build();
}
SHARED.with(Clone::clone)
}
}
}
impl ReqwestClient {
pub fn inner(&self) -> Option<&reqwest::Client> {
match &self.0 {
Built::Client(client) => Some(client),
Built::Failed(_) => None,
}
}
#[cfg(test)]
fn same(&self, other: &Self) -> bool {
match (&self.0, &other.0) {
(Built::Client(a), Built::Client(b)) => Arc::ptr_eq(a, b),
(Built::Failed(a), Built::Failed(b)) => Arc::ptr_eq(a, b),
_ => false,
}
}
}
#[cfg(all(feature = "rustls", not(target_family = "wasm")))]
fn with_bundled_roots(error: reqwest::Error) -> reqwest::Result<reqwest::Client> {
let roots = webpki_root_certs::TLS_SERVER_ROOT_CERTS
.iter()
.filter_map(|der| reqwest::Certificate::from_der(der).ok());
let Ok(client) = reqwest::Client::builder().tls_certs_only(roots).build() else {
return Err(error);
};
tracing::warn!(
"the system's CA certificates could not be loaded ({}), so rig trusts its bundled \
Mozilla roots; install the system's CA certificates, or set SSL_CERT_FILE or \
SSL_CERT_DIR, to trust those instead",
Chain(&error)
);
Ok(client)
}
fn unbuilt(error: &Arc<reqwest::Error>) -> Error {
Error::unbuilt(TransportBuildError(Arc::clone(error)))
}
impl From<reqwest::Client> for ReqwestClient {
fn from(client: reqwest::Client) -> Self {
Self(Built::Client(Arc::new(client)))
}
}
use bytes::Bytes;
use rig_http::http_client::{
DynHttpClient, Error, HttpClientExt, LazyBody, MultipartForm, Request, Response, Result,
StreamingResponse, multipart::PartContent,
};
use rig_http::wasm_compat::*;
use std::pin::Pin;
use std::sync::Arc;
#[derive(Debug)]
struct TransportBuildError(Arc<reqwest::Error>);
impl std::fmt::Display for TransportBuildError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"could not build the bundled reqwest transport: {}",
Chain(&*self.0)
)
}
}
struct Chain<'a>(&'a (dyn std::error::Error + 'static));
impl std::fmt::Display for Chain<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)?;
let mut source = self.0.source();
while let Some(cause) = source {
write!(f, ": {cause}")?;
source = cause.source();
}
Ok(())
}
}
impl std::error::Error for TransportBuildError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(&*self.0)
}
}
pub fn from_reqwest(err: reqwest::Error) -> Error {
Error::instance(err)
}
async fn non_success_status_error(response: reqwest::Response) -> Error {
let status = response.status();
let headers = response.headers().clone();
let body = response
.text()
.await
.unwrap_or_else(|error| format!("failed to read error response body: {error}"));
Error::non_success_with_details(status, headers, body)
}
async fn into_response<U>(mut response: reqwest::Response) -> Result<Response<LazyBody<U>>>
where
U: From<Bytes>,
U: WasmCompatSend + 'static,
{
if !response.status().is_success() {
return Err(non_success_status_error(response).await);
}
let mut res = Response::builder().status(response.status());
if let Some(headers) = res.headers_mut() {
*headers = std::mem::take(response.headers_mut());
}
let body = async {
let bytes = response.bytes().await.map_err(Error::instance)?;
Ok(U::from(bytes))
};
#[cfg(not(target_family = "wasm"))]
let body = rig_http::runtime::bind(body)?;
let body: LazyBody<U> = Box::pin(body);
res.body(body).map_err(Error::Protocol)
}
fn streaming_head(response: &mut reqwest::Response) -> http::response::Builder {
#[cfg(not(target_family = "wasm"))]
let mut res = Response::builder()
.status(response.status())
.version(response.version());
#[cfg(target_family = "wasm")]
let mut res = Response::builder().status(response.status());
if let Some(hs) = res.headers_mut() {
*hs = std::mem::take(response.headers_mut());
}
res
}
async fn into_streaming_response(mut response: reqwest::Response) -> Result<StreamingResponse> {
if !response.status().is_success() {
return Err(non_success_status_error(response).await);
}
let res = streaming_head(&mut response);
use futures::StreamExt;
let stream = response
.bytes_stream()
.map(|chunk| chunk.map_err(Error::instance));
#[cfg(not(target_family = "wasm"))]
let stream = rig_http::runtime::bind_stream(stream)?;
let stream: Pin<Box<dyn WasmCompatSendStream<InnerItem = Result<Bytes>>>> = Box::pin(stream);
res.body(stream).map_err(Error::Protocol)
}
#[derive(Debug, thiserror::Error)]
#[error("multipart part {part:?} has an unusable content type {content_type:?}: {source}")]
struct InvalidPartContentType {
part: String,
content_type: String,
source: reqwest::Error,
}
pub fn multipart_form(value: MultipartForm) -> Result<reqwest::multipart::Form> {
let mut form = reqwest::multipart::Form::new();
for part in value.into_parts() {
let (name, content, filename, content_type) = part.into_pieces();
match content {
PartContent::Text(text) => {
form = form.text(name, text);
}
PartContent::Binary(bytes) => {
let mut req_part = reqwest::multipart::Part::stream(bytes);
if let Some(content_type) = content_type.as_ref() {
req_part = req_part.mime_str(content_type.as_ref()).map_err(|source| {
Error::instance(InvalidPartContentType {
part: name.clone(),
content_type: content_type.as_ref().to_string(),
source,
})
})?;
}
if let Some(filename) = filename {
req_part = req_part.file_name(filename);
}
form = form.part(name, req_part);
}
}
}
Ok(form)
}
enum Payload {
Bytes(Bytes),
Form(reqwest::multipart::Form),
}
trait Execute: WasmCompatSend + WasmCompatSync {
fn execute(
&self,
parts: http::request::Parts,
payload: Result<Payload>,
) -> Result<impl Future<Output = Result<reqwest::Response>> + WasmCompatSend + 'static>;
}
impl Execute for ReqwestClient {
fn execute(
&self,
parts: http::request::Parts,
payload: Result<Payload>,
) -> Result<impl Future<Output = Result<reqwest::Response>> + WasmCompatSend + 'static> {
let client = match &self.0 {
Built::Client(client) => client,
Built::Failed(error) => return Err(unbuilt(error)),
};
let request = client
.request(parts.method, parts.uri.to_string())
.headers(parts.headers);
let request = match payload? {
Payload::Bytes(bytes) => request.body(bytes),
Payload::Form(form) => request.multipart(form),
};
Ok(async move { request.send().await.map_err(Error::instance) })
}
}
fn send_with<C, T, F>(
client: &C,
parts: http::request::Parts,
payload: Result<Payload>,
convert: impl FnOnce(reqwest::Response) -> F + WasmCompatSend + 'static,
) -> impl Future<Output = Result<T>> + WasmCompatSend + 'static
where
C: Execute,
F: Future<Output = Result<T>> + WasmCompatSend,
{
let sent = client.execute(parts, payload);
async move {
let sent = sent?;
let operation = async move { convert(sent.await?).await };
#[cfg(not(target_family = "wasm"))]
let operation = rig_http::runtime::bind(operation)?;
operation.await
}
}
macro_rules! http_client_ext {
($client:ty) => {
impl HttpClientExt for $client {
fn send<T, U>(
&self,
req: Request<T>,
) -> impl Future<Output = Result<Response<LazyBody<U>>>> + WasmCompatSend + 'static
where
T: Into<Bytes>,
U: From<Bytes> + WasmCompatSend + 'static,
{
let (parts, body) = req.into_parts();
let payload = Ok(Payload::Bytes(body.into()));
send_with(self, parts, payload, into_response::<U>)
}
fn send_multipart<U>(
&self,
req: Request<MultipartForm>,
) -> impl Future<Output = Result<Response<LazyBody<U>>>> + WasmCompatSend + 'static
where
U: From<Bytes> + WasmCompatSend + 'static,
{
let (parts, form) = req.into_parts();
let payload = multipart_form(form).map(Payload::Form);
send_with(self, parts, payload, into_response::<U>)
}
fn send_streaming<T>(
&self,
req: Request<T>,
) -> impl Future<Output = Result<StreamingResponse>> + WasmCompatSend
where
T: Into<Bytes> + WasmCompatSend,
{
let (parts, body) = req.into_parts();
let payload = Ok(Payload::Bytes(body.into()));
send_with(self, parts, payload, into_streaming_response)
}
}
};
}
http_client_ext!(ReqwestClient);
#[cfg(any(
feature = "reqwest-middleware-rustls",
feature = "reqwest-middleware-native-tls"
))]
mod middleware;
#[cfg(any(
feature = "reqwest-middleware-rustls",
feature = "reqwest-middleware-native-tls"
))]
pub use middleware::ReqwestMiddlewareClient;
#[cfg(not(target_family = "wasm"))]
const _: fn() = || {
fn assert_send_sync_static<T: Send + Sync + 'static>() {}
assert_send_sync_static::<ReqwestClient>();
};
#[cfg(test)]
mod tests;