Skip to main content

rig_http/http_client/
mod.rs

1//! Transport-independent HTTP requests, lazy response bodies, and errors.
2//!
3//! ```
4//! use rig_http::http_client::{HeaderMap, bearer_auth_header};
5//!
6//! let mut headers = HeaderMap::new();
7//! bearer_auth_header(&mut headers, "example-token")?;
8//! # Ok::<(), rig_http::http_client::Error>(())
9//! ```
10
11use bytes::Bytes;
12use http::HeaderName;
13pub use http::{
14    HeaderMap, HeaderValue, Method, Request, Response, StatusCode, Uri, request::Builder,
15};
16mod erased;
17pub mod framing;
18pub mod middleware;
19pub mod multipart;
20use crate::wasm_compat::*;
21pub use erased::DynHttpClient;
22pub use middleware::HttpMiddleware;
23pub use multipart::MultipartForm;
24
25#[derive(Debug, thiserror::Error)]
26pub enum Error {
27    #[error("Http error: {0}")]
28    Protocol(#[from] http::Error),
29    /// Failed HTTP response preserving its status, body, and headers.
30    /// Transport failures without a response use [`Self::Instance`] or a
31    /// protocol variant.
32    #[error("Invalid status code {status} with message: {body}")]
33    InvalidStatusCodeWithDetails {
34        /// The non-success status.
35        status: StatusCode,
36        /// The raw response body.
37        body: String,
38        /// The failed response's headers, verbatim.
39        headers: http::HeaderMap,
40    },
41    #[error("Header value outside of legal range: {0}")]
42    InvalidHeaderValue(#[from] http::header::InvalidHeaderValue),
43    #[error("Request in error state, cannot access headers")]
44    NoHeaders,
45    #[error("Stream ended")]
46    StreamEnded,
47    #[error("Invalid content type was returned: {0:?}")]
48    InvalidContentType(HeaderValue),
49    #[cfg(not(target_family = "wasm"))]
50    #[error("Http client error: {0}")]
51    Instance(#[from] Box<dyn std::error::Error + Send + Sync + 'static>),
52
53    #[cfg(target_family = "wasm")]
54    #[error("Http client error: {0}")]
55    Instance(#[from] Box<dyn std::error::Error + 'static>),
56}
57
58impl Error {
59    /// Preserved HTTP failure status, or `None` for other error variants.
60    pub fn non_success_status(&self) -> Option<StatusCode> {
61        match self {
62            Self::InvalidStatusCodeWithDetails { status, .. } => Some(*status),
63            _ => None,
64        }
65    }
66
67    /// The response body this error preserved, when it has one. Companion to
68    /// [`Self::non_success_status`] and [`Self::non_success_headers`].
69    pub fn non_success_body(&self) -> Option<&str> {
70        match self {
71            Self::InvalidStatusCodeWithDetails { body, .. } => Some(body.as_str()),
72            _ => None,
73        }
74    }
75
76    /// Constructs a failure from its HTTP status, headers, and body without
77    /// validating that the status is non-successful.
78    pub fn non_success_with_details(status: StatusCode, headers: HeaderMap, body: String) -> Self {
79        Self::InvalidStatusCodeWithDetails {
80            status,
81            body,
82            headers,
83        }
84    }
85
86    /// Returns the failed response's headers, when this error preserved them.
87    ///
88    /// The following example reads the seconds form of `Retry-After`:
89    ///
90    /// ```
91    /// # use rig_http::http_client::Error;
92    /// # use std::time::Duration;
93    /// fn retry_after(error: &Error) -> Option<Duration> {
94    ///     let seconds = error
95    ///         .non_success_headers()?
96    ///         .get(http::header::RETRY_AFTER)?
97    ///         .to_str()
98    ///         .ok()?
99    ///         .parse()
100    ///         .ok()?;
101    ///     Some(Duration::from_secs(seconds))
102    /// }
103    /// ```
104    ///
105    /// Returns `None` for a response-less failure: every non-success error
106    /// carries its headers.
107    pub fn non_success_headers(&self) -> Option<&HeaderMap> {
108        match self {
109            Self::InvalidStatusCodeWithDetails { headers, .. } => Some(headers),
110            _ => None,
111        }
112    }
113}
114
115pub type Result<T> = std::result::Result<T, Error>;
116
117impl Error {
118    /// Wrap a transport's native error as [`Error::Instance`]. Transports use
119    /// this for response-less failures (connect, decode, timeout); non-success
120    /// responses go through [`Error::non_success_with_details`] instead so the
121    /// status stays inspectable.
122    #[cfg(not(target_family = "wasm"))]
123    pub fn instance<E: std::error::Error + Send + Sync + 'static>(error: E) -> Self {
124        Self::Instance(error.into())
125    }
126
127    /// Wrap a transport's native error as [`Error::Instance`].
128    #[cfg(target_family = "wasm")]
129    pub fn instance<E: std::error::Error + 'static>(error: E) -> Self {
130        Self::Instance(error.into())
131    }
132}
133
134pub type LazyBytes = WasmBoxedFuture<'static, Result<Bytes>>;
135pub type LazyBody<T> = WasmBoxedFuture<'static, Result<T>>;
136
137/// The body of a streaming response: the transport's own chunks, boxed.
138pub type BoxedStream = std::pin::Pin<Box<dyn WasmCompatSendStream<InnerItem = Result<Bytes>>>>;
139
140pub type StreamingResponse = Response<BoxedStream>;
141
142#[derive(Debug, Clone, Copy)]
143pub struct NoBody;
144
145impl From<NoBody> for Bytes {
146    fn from(_: NoBody) -> Self {
147        Bytes::new()
148    }
149}
150
151pub async fn text(response: Response<LazyBody<Vec<u8>>>) -> Result<String> {
152    let text = response.into_body().await?;
153    Ok(String::from(String::from_utf8_lossy(&text)))
154}
155
156pub fn make_auth_header(key: impl AsRef<str>) -> Result<(HeaderName, HeaderValue)> {
157    Ok((
158        http::header::AUTHORIZATION,
159        HeaderValue::from_str(&format!("Bearer {}", key.as_ref()))?,
160    ))
161}
162
163pub fn bearer_auth_header(headers: &mut HeaderMap, key: impl AsRef<str>) -> Result<()> {
164    let (k, v) = make_auth_header(key)?;
165
166    headers.insert(k, v);
167
168    Ok(())
169}
170
171/// An HTTP client Rig sends through: unary, multipart and streaming requests.
172///
173/// `rig-reqwest`'s `ReqwestClient` is the bundled implementation; a provider
174/// configuration takes any implementation with `with_http(client)`.
175#[diagnostic::on_unimplemented(
176    message = "`{Self}` is not an HTTP client Rig can send through",
177    label = "not an `HttpClientExt`",
178    note = "use `rig_reqwest::ReqwestClient` (wrap a configured `reqwest::Client` with `ReqwestClient::from(client)`), erase a client with `DynHttpClient::new(client)`, or implement `HttpClientExt`; a provider configuration takes it with `.with_http(client)`"
179)]
180pub trait HttpClientExt: WasmCompatSend + WasmCompatSync {
181    /// Sends a request and returns headers with a lazy body converted from bytes to `U`.
182    fn send<T, U>(
183        &self,
184        req: Request<T>,
185    ) -> impl Future<Output = Result<Response<LazyBody<U>>>> + WasmCompatSend + 'static
186    where
187        T: Into<Bytes>,
188        T: WasmCompatSend,
189        U: From<Bytes>,
190        U: WasmCompatSend + 'static;
191
192    /// Sends a multipart request and returns headers with a lazy body converted to `U`.
193    fn send_multipart<U>(
194        &self,
195        req: Request<MultipartForm>,
196    ) -> impl Future<Output = Result<Response<LazyBody<U>>>> + WasmCompatSend + 'static
197    where
198        U: From<Bytes>,
199        U: WasmCompatSend + 'static;
200
201    /// Sends a request and returns headers with a stream of transport byte chunks.
202    fn send_streaming<T>(
203        &self,
204        req: Request<T>,
205    ) -> impl Future<Output = Result<StreamingResponse>> + WasmCompatSend
206    where
207        T: Into<Bytes> + WasmCompatSend;
208}
209
210#[cfg(test)]
211mod non_success_header_tests;