1use crate::http_client::sse::BoxedStream;
2use bytes::Bytes;
3pub use http::{HeaderMap, HeaderValue, Method, Request, Response, Uri, request::Builder};
4use http::{HeaderName, StatusCode};
5use reqwest::Body;
6pub mod multipart;
7pub mod retry;
8pub mod sse;
9use crate::wasm_compat::*;
10pub use multipart::MultipartForm;
11pub use reqwest::Client as ReqwestClient;
12use std::pin::Pin;
13
14#[derive(Debug, thiserror::Error)]
15pub enum Error {
16 #[error("Http error: {0}")]
17 Protocol(#[from] http::Error),
18 #[error("Invalid status code: {0}")]
19 InvalidStatusCode(StatusCode),
20 #[error("Invalid status code {0} with message: {1}")]
21 InvalidStatusCodeWithMessage(StatusCode, String),
22 #[error("Invalid status code {status} with message: {body}")]
27 InvalidStatusCodeWithDetails {
28 status: StatusCode,
30 body: String,
32 headers: Box<http::HeaderMap>,
34 },
35 #[error("Header value outside of legal range: {0}")]
36 InvalidHeaderValue(#[from] http::header::InvalidHeaderValue),
37 #[error("Request in error state, cannot access headers")]
38 NoHeaders,
39 #[error("Stream ended")]
40 StreamEnded,
41 #[error("Invalid content type was returned: {0:?}")]
42 InvalidContentType(HeaderValue),
43 #[cfg(not(target_family = "wasm"))]
44 #[error("Http client error: {0}")]
45 Instance(#[from] Box<dyn std::error::Error + Send + Sync + 'static>),
46
47 #[cfg(target_family = "wasm")]
48 #[error("Http client error: {0}")]
49 Instance(#[from] Box<dyn std::error::Error + 'static>),
50}
51
52impl Error {
53 pub(crate) fn non_success_status(&self) -> Option<StatusCode> {
54 match self {
55 Self::InvalidStatusCode(status) | Self::InvalidStatusCodeWithMessage(status, _) => {
56 Some(*status)
57 }
58 Self::InvalidStatusCodeWithDetails { status, .. } => Some(*status),
59 _ => None,
60 }
61 }
62
63 pub(crate) fn non_success_body(&self) -> Option<&str> {
64 match self {
65 Self::InvalidStatusCodeWithMessage(_, body) => Some(body.as_str()),
66 Self::InvalidStatusCodeWithDetails { body, .. } => Some(body.as_str()),
67 _ => None,
68 }
69 }
70
71 pub fn non_success_headers(&self) -> Option<&HeaderMap> {
98 match self {
99 Self::InvalidStatusCodeWithDetails { headers, .. } => Some(headers),
100 _ => None,
101 }
102 }
103}
104
105pub type Result<T> = std::result::Result<T, Error>;
106
107#[cfg(not(target_family = "wasm"))]
108pub(crate) fn instance_error<E: std::error::Error + Send + Sync + 'static>(error: E) -> Error {
109 Error::Instance(error.into())
110}
111
112#[cfg(target_family = "wasm")]
113fn instance_error<E: std::error::Error + 'static>(error: E) -> Error {
114 Error::Instance(error.into())
115}
116
117async fn non_success_status_error(response: reqwest::Response) -> Error {
118 let status = response.status();
119 let headers = Box::new(response.headers().clone());
123 let body = response
124 .text()
125 .await
126 .unwrap_or_else(|error| format!("failed to read error response body: {error}"));
127 Error::InvalidStatusCodeWithDetails {
128 status,
129 body,
130 headers,
131 }
132}
133
134pub type LazyBytes = WasmBoxedFuture<'static, Result<Bytes>>;
135pub type LazyBody<T> = WasmBoxedFuture<'static, Result<T>>;
136
137pub type StreamingResponse = Response<BoxedStream>;
138
139#[derive(Debug, Clone, Copy)]
140pub struct NoBody;
141
142impl From<NoBody> for Bytes {
143 fn from(_: NoBody) -> Self {
144 Bytes::new()
145 }
146}
147
148impl From<NoBody> for Body {
149 fn from(_: NoBody) -> Self {
150 reqwest::Body::default()
151 }
152}
153
154pub async fn text(response: Response<LazyBody<Vec<u8>>>) -> Result<String> {
155 let text = response.into_body().await?;
156 Ok(String::from(String::from_utf8_lossy(&text)))
157}
158
159pub fn make_auth_header(key: impl AsRef<str>) -> Result<(HeaderName, HeaderValue)> {
160 Ok((
161 http::header::AUTHORIZATION,
162 HeaderValue::from_str(&format!("Bearer {}", key.as_ref()))?,
163 ))
164}
165
166pub fn bearer_auth_header(headers: &mut HeaderMap, key: impl AsRef<str>) -> Result<()> {
167 let (k, v) = make_auth_header(key)?;
168
169 headers.insert(k, v);
170
171 Ok(())
172}
173
174pub trait HttpClientExt: WasmCompatSend + WasmCompatSync {
176 fn send<T, U>(
178 &self,
179 req: Request<T>,
180 ) -> impl Future<Output = Result<Response<LazyBody<U>>>> + WasmCompatSend + 'static
181 where
182 T: Into<Bytes>,
183 T: WasmCompatSend,
184 U: From<Bytes>,
185 U: WasmCompatSend + 'static;
186
187 fn send_multipart<U>(
189 &self,
190 req: Request<MultipartForm>,
191 ) -> impl Future<Output = Result<Response<LazyBody<U>>>> + WasmCompatSend + 'static
192 where
193 U: From<Bytes>,
194 U: WasmCompatSend + 'static;
195
196 fn send_streaming<T>(
198 &self,
199 req: Request<T>,
200 ) -> impl Future<Output = Result<StreamingResponse>> + WasmCompatSend
201 where
202 T: Into<Bytes> + WasmCompatSend;
203}
204
205async fn into_lazy_response<U>(response: reqwest::Response) -> Result<Response<LazyBody<U>>>
206where
207 U: From<Bytes>,
208 U: WasmCompatSend + 'static,
209{
210 if !response.status().is_success() {
211 return Err(non_success_status_error(response).await);
212 }
213
214 let mut res = Response::builder().status(response.status());
215
216 if let Some(headers) = res.headers_mut() {
217 *headers = response.headers().clone();
218 }
219
220 let body: LazyBody<U> = Box::pin(async {
221 let bytes = response.bytes().await.map_err(instance_error)?;
222 Ok(U::from(bytes))
223 });
224
225 res.body(body).map_err(Error::Protocol)
226}
227
228macro_rules! impl_http_client_ext {
229 ($(#[$attribute:meta])* $client:ty) => {
230 $(#[$attribute])*
231 impl HttpClientExt for $client {
232 fn send<T, U>(
233 &self,
234 req: Request<T>,
235 ) -> impl Future<Output = Result<Response<LazyBody<U>>>> + WasmCompatSend + 'static
236 where
237 T: Into<Bytes>,
238 U: From<Bytes> + WasmCompatSend + 'static,
239 {
240 let (parts, body) = req.into_parts();
241 let req = self
242 .request(parts.method, parts.uri.to_string())
243 .headers(parts.headers)
244 .body(body.into());
245
246 async move {
247 let response = req.send().await.map_err(instance_error)?;
248 into_lazy_response(response).await
249 }
250 }
251
252 fn send_multipart<U>(
253 &self,
254 req: Request<MultipartForm>,
255 ) -> impl Future<Output = Result<Response<LazyBody<U>>>> + WasmCompatSend + 'static
256 where
257 U: From<Bytes>,
258 U: WasmCompatSend + 'static,
259 {
260 let (parts, body) = req.into_parts();
261 let body = reqwest::multipart::Form::from(body);
262
263 let req = self
264 .request(parts.method, parts.uri.to_string())
265 .headers(parts.headers)
266 .multipart(body);
267
268 async move {
269 let response = req.send().await.map_err(instance_error)?;
270 into_lazy_response(response).await
271 }
272 }
273
274 fn send_streaming<T>(
275 &self,
276 req: Request<T>,
277 ) -> impl Future<Output = Result<StreamingResponse>> + WasmCompatSend
278 where
279 T: Into<Bytes> + WasmCompatSend,
280 {
281 let (parts, body) = req.into_parts();
282
283 let client = self.clone();
284
285 async move {
286 let req = self
287 .request(parts.method, parts.uri.to_string())
288 .headers(parts.headers)
289 .body(body.into())
290 .build()
291 .map_err(|error| Error::Instance(error.into()))?;
292 let response: reqwest::Response =
293 client.execute(req).await.map_err(instance_error)?;
294 if !response.status().is_success() {
295 return Err(non_success_status_error(response).await);
296 }
297
298 #[cfg(not(target_family = "wasm"))]
299 let mut res = Response::builder()
300 .status(response.status())
301 .version(response.version());
302
303 #[cfg(target_family = "wasm")]
304 let mut res = Response::builder().status(response.status());
305
306 if let Some(hs) = res.headers_mut() {
307 *hs = response.headers().clone();
308 }
309
310 use futures::StreamExt;
311
312 let mapped_stream: Pin<
313 Box<dyn WasmCompatSendStream<InnerItem = Result<Bytes>>>,
314 > = Box::pin(
315 response
316 .bytes_stream()
317 .map(|chunk| chunk.map_err(|e| Error::Instance(Box::new(e)))),
318 );
319
320 res.body(mapped_stream).map_err(Error::Protocol)
321 }
322 }
323 }
324 };
325}
326
327impl_http_client_ext!(reqwest::Client);
328
329impl_http_client_ext!(
330 #[cfg(feature = "reqwest-middleware")]
331 #[cfg_attr(docsrs, doc(cfg(feature = "reqwest-middleware")))]
332 reqwest_middleware::ClientWithMiddleware
333);
334
335#[cfg(test)]
336mod non_success_header_tests {
337 use super::*;
338
339 #[tokio::test]
342 async fn non_success_status_error_preserves_response_headers() {
343 let response = http::Response::builder()
344 .status(StatusCode::TOO_MANY_REQUESTS)
345 .header("retry-after", "20")
346 .header("x-ratelimit-remaining", "0")
347 .body(r#"{"error":{"message":"rate limited"}}"#)
348 .expect("valid response");
349
350 let error = non_success_status_error(reqwest::Response::from(response)).await;
351
352 assert_eq!(
353 error.non_success_status(),
354 Some(StatusCode::TOO_MANY_REQUESTS)
355 );
356 assert_eq!(
357 error.non_success_body(),
358 Some(r#"{"error":{"message":"rate limited"}}"#)
359 );
360 let headers = error
361 .non_success_headers()
362 .expect("headers captured at error construction");
363 assert_eq!(
364 headers.get("retry-after").and_then(|v| v.to_str().ok()),
365 Some("20")
366 );
367 assert_eq!(
368 headers
369 .get("x-ratelimit-remaining")
370 .and_then(|v| v.to_str().ok()),
371 Some("0")
372 );
373 }
374
375 #[test]
378 fn non_success_headers_absent_when_not_captured() {
379 for error in [
380 Error::InvalidStatusCodeWithMessage(
381 StatusCode::TOO_MANY_REQUESTS,
382 "rate limited".to_string(),
383 ),
384 Error::InvalidStatusCode(StatusCode::TOO_MANY_REQUESTS),
385 Error::StreamEnded,
386 ] {
387 assert!(error.non_success_headers().is_none());
388 }
389
390 let error = Error::InvalidStatusCodeWithDetails {
392 status: StatusCode::TOO_MANY_REQUESTS,
393 body: "rate limited".to_string(),
394 headers: Box::new(HeaderMap::new()),
395 };
396 assert!(error.non_success_headers().is_some_and(HeaderMap::is_empty));
397 }
398}