1use crate::Error;
2use bytes::Bytes;
3use http::{
4 Method,
5 header::{CONTENT_TYPE, HeaderMap, HeaderName, HeaderValue},
6};
7use http_body_util::{BodyExt, Full};
8use hyper_rustls::HttpsConnector;
9use hyper_util::{
10 client::legacy::{Client, connect::HttpConnector},
11 rt::TokioExecutor,
12};
13use serde::{Serialize, de::DeserializeOwned};
14use std::time::Duration;
15
16pub use http::Method as HttpMethod;
18pub use http::header::{HeaderMap as HttpHeaderMap, HeaderValue as HttpHeaderValue};
19
20pub type HyperClient = Client<HttpsConnector<HttpConnector>, Full<Bytes>>;
21
22#[derive(Debug, Clone)]
23pub struct HttpClientBuilder {
24 timeout: Duration,
25 headers: HeaderMap,
26}
27
28#[derive(Clone)]
29pub struct HttpClient {
30 headers: HeaderMap,
31 client: HyperClient,
32 timeout: Duration,
33}
34
35#[derive(Clone)]
36pub struct HttpRequest {
37 method: Method,
38 url: String,
39 headers: HeaderMap,
40 body: Option<String>,
41 client: HyperClient,
42 timeout: Duration,
43}
44
45impl std::fmt::Debug for HttpClient {
46 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47 f.debug_struct("HttpClient")
48 .field("headers", &self.headers)
49 .field("timeout", &self.timeout)
50 .finish_non_exhaustive()
51 }
52}
53
54impl std::fmt::Debug for HttpRequest {
55 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56 f.debug_struct("HttpRequest")
57 .field("method", &self.method)
58 .field("url", &self.url)
59 .field("headers", &self.headers)
60 .field("body", &self.body)
61 .finish_non_exhaustive()
62 }
63}
64
65impl Default for HttpClientBuilder {
66 fn default() -> Self {
67 Self::new()
68 }
69}
70
71impl HttpClientBuilder {
72 pub fn new() -> Self {
73 let mut headers = HeaderMap::new();
74 headers.append(CONTENT_TYPE, HeaderValue::from_static("application/json"));
75
76 Self {
77 timeout: Duration::from_secs(30),
78 headers,
79 }
80 }
81
82 pub fn with_header(mut self, name: &'static str, value: impl AsRef<str>) -> Self {
83 if let (Ok(val), Ok(n)) = (
84 HeaderValue::from_str(value.as_ref()),
85 name.parse::<HeaderName>(),
86 ) {
87 self.headers.append(n, val);
88 }
89 self
90 }
91
92 pub fn set_header(mut self, name: &'static str, value: impl AsRef<str>) -> Self {
93 if let (Ok(val), Ok(n)) = (
94 HeaderValue::from_str(value.as_ref()),
95 name.parse::<HeaderName>(),
96 ) {
97 self.headers.insert(n, val);
98 }
99 self
100 }
101
102 pub fn without_header(mut self, name: &'static str) -> Self {
103 if let Ok(n) = name.parse::<HeaderName>() {
104 self.headers.remove(n);
105 } else {
106 self.headers.remove(name);
107 }
108 self
109 }
110
111 pub fn with_timeout(mut self, timeout: Option<Duration>) -> Self {
112 if let Some(timeout) = timeout {
113 self.timeout = timeout;
114 }
115 self
116 }
117
118 pub fn build(self) -> HttpClient {
119 let connector = build_https_connector();
120 let client = Client::builder(TokioExecutor::new()).build(connector);
121 HttpClient {
122 headers: self.headers,
123 client,
124 timeout: self.timeout,
125 }
126 }
127}
128
129impl HttpClient {
130 pub fn request(&self, method: Method, url: impl Into<String>) -> HttpRequest {
131 HttpRequest {
132 method,
133 url: url.into(),
134 headers: self.headers.clone(),
135 body: None,
136 client: self.client.clone(),
137 timeout: self.timeout,
138 }
139 }
140
141 pub fn get(&self, url: impl Into<String>) -> HttpRequest {
142 self.request(Method::GET, url)
143 }
144
145 pub fn post(&self, url: impl Into<String>) -> HttpRequest {
146 self.request(Method::POST, url)
147 }
148
149 pub fn put(&self, url: impl Into<String>) -> HttpRequest {
150 self.request(Method::PUT, url)
151 }
152
153 pub fn delete(&self, url: impl Into<String>) -> HttpRequest {
154 self.request(Method::DELETE, url)
155 }
156
157 pub fn patch(&self, url: impl Into<String>) -> HttpRequest {
158 self.request(Method::PATCH, url)
159 }
160}
161
162impl HttpRequest {
163 pub fn with_header(mut self, name: &'static str, value: impl AsRef<str>) -> Self {
164 if let (Ok(val), Ok(n)) = (
165 HeaderValue::from_str(value.as_ref()),
166 name.parse::<HeaderName>(),
167 ) {
168 self.headers.append(n, val);
169 }
170 self
171 }
172
173 pub fn set_header(mut self, name: &'static str, value: impl AsRef<str>) -> Self {
174 if let (Ok(val), Ok(n)) = (
175 HeaderValue::from_str(value.as_ref()),
176 name.parse::<HeaderName>(),
177 ) {
178 self.headers.insert(n, val);
179 }
180 self
181 }
182
183 pub fn with_body<B: Serialize>(mut self, body: B) -> crate::Result<Self> {
184 match serde_json::to_string(&body) {
185 Ok(body) => {
186 self.body = Some(body);
187 Ok(self)
188 }
189 Err(err) => Err(Error::Serialize(format!(
190 "Failed to serialize request: {err}"
191 ))),
192 }
193 }
194
195 pub fn with_raw_body(mut self, body: String) -> Self {
196 self.body = Some(body);
197 self
198 }
199
200 pub async fn send<T>(self) -> crate::Result<T>
201 where
202 T: DeserializeOwned,
203 {
204 let response = self.send_raw().await?;
205 serde_json::from_slice::<T>(response.as_bytes()).map_err(|err| {
206 Error::Serialize(format!(
207 "Failed to deserialize response: {err} (body: {})",
208 body_snippet(&response)
209 ))
210 })
211 }
212
213 pub async fn send_raw(self) -> crate::Result<String> {
214 self.send_raw_with_headers().await.map(|(body, _)| body)
215 }
216
217 pub async fn send_raw_with_headers(self) -> crate::Result<(String, HeaderMap)> {
218 let url = self.url.clone();
219 let timeout = self.timeout;
220 let body_opt = self.body.clone();
221 let method = self.method.clone();
222 let headers = self.headers.clone();
223 let client = self.client.clone();
224
225 let body_bytes = body_opt.map(Bytes::from).unwrap_or_default();
226 let full = Full::new(body_bytes);
227
228 let mut builder = http::Request::builder().method(method).uri(url.as_str());
229 for (k, v) in headers.iter() {
230 builder = builder.header(k, v);
231 }
232 let req = builder
233 .body(full)
234 .map_err(|e| Error::Api(format!("Failed to build request to {url}: {e}")))?;
235
236 let resp = tokio::time::timeout(timeout, client.request(req))
237 .await
238 .map_err(|_| Error::Api(format!("Request to {url} timed out after {timeout:?}")))?
239 .map_err(|e| Error::Api(format!("Failed to send request to {url}: {e}")))?;
240
241 let status = resp.status();
242 let resp_headers = resp.headers().clone();
243 let collected = resp
244 .collect()
245 .await
246 .map_err(|e| Error::Api(format!("Failed to read response from {url}: {e}")))?;
247 let bytes = collected.to_bytes();
248 let body_str = String::from_utf8_lossy(&bytes).to_string();
249
250 let code = status.as_u16();
251 match code {
252 204 => Ok((String::new(), resp_headers)),
253 200..=299 => Ok((body_str, resp_headers)),
254 401 => Err(Error::Unauthorized),
255 404 => Err(Error::NotFound),
256 _ => Err(Error::Api(http_status_message(code, &body_str))),
257 }
258 }
259
260 pub async fn send_with_retry<T>(self, max_retries: u32) -> crate::Result<T>
261 where
262 T: DeserializeOwned,
263 {
264 let mut attempts: u32 = 0;
265 let Self {
267 method,
268 url,
269 headers,
270 body,
271 client,
272 timeout,
273 } = self;
274
275 loop {
276 let body_bytes = body.clone().map(Bytes::from).unwrap_or_default();
277 let full = Full::new(body_bytes);
278 let mut builder = http::Request::builder()
279 .method(method.clone())
280 .uri(url.as_str());
281 for (k, v) in headers.iter() {
282 builder = builder.header(k, v);
283 }
284 let req = builder
285 .body(full)
286 .map_err(|e| Error::Api(format!("Failed to build request to {url}: {e}")))?;
287
288 let resp = tokio::time::timeout(timeout, client.request(req))
289 .await
290 .map_err(|_| Error::Api(format!("Request to {url} timed out after {timeout:?}")))?
291 .map_err(|e| Error::Api(format!("Failed to send request to {url}: {e}")))?;
292
293 let status = resp.status();
294 let resp_headers = resp.headers().clone();
295 let collected = resp
296 .collect()
297 .await
298 .map_err(|e| Error::Api(format!("Failed to read response from {url}: {e}")))?;
299 let bytes = collected.to_bytes();
300 let text = String::from_utf8_lossy(&bytes).to_string();
301 let code = status.as_u16();
302
303 match code {
304 204 => {
305 return serde_json::from_str("{}").map_err(|err| {
306 Error::Serialize(format!("Failed to create empty response: {err}"))
307 });
308 }
309 200..=299 => {
310 let parse_target = if text.trim().is_empty() { "{}" } else { &text };
311 return serde_json::from_str(parse_target).map_err(|err| {
312 Error::Serialize(format!(
313 "Failed to deserialize response from {}: {err} (body: {})",
314 url,
315 body_snippet(&text)
316 ))
317 });
318 }
319 429 | 503 if attempts < max_retries => {
320 let delay = retry_after(&resp_headers)
321 .unwrap_or_else(|| Duration::from_secs(1u64 << attempts.min(6)));
322 tokio::time::sleep(delay.min(MAX_RETRY_DELAY)).await;
323 attempts += 1;
324 continue;
325 }
326 401 => return Err(Error::Unauthorized),
327 404 => return Err(Error::NotFound),
328 _ => {
329 return Err(Error::Api(http_status_message(code, &text)));
330 }
331 }
332 }
333 }
334}
335
336pub(crate) fn build_https_connector() -> HttpsConnector<HttpConnector> {
350 install_crypto_provider();
351
352 #[cfg(feature = "rustls-platform-verifier")]
354 {
355 if let Ok(connector) = try_build_with_platform_verifier() {
356 return connector;
357 }
358 }
360
361 #[cfg(feature = "native-tokio")]
362 {
363 if let Ok(connector) = try_build_with_native_roots() {
364 return connector;
365 }
366 }
368
369 #[cfg(feature = "webpki-tokio")]
370 {
371 return build_with_webpki_roots();
372 }
373
374 #[cfg(not(any(
375 feature = "webpki-tokio",
376 feature = "native-tokio",
377 feature = "rustls-platform-verifier"
378 )))]
379 {
380 compile_error!(
381 "At least one verifier feature must be enabled: webpki-tokio, native-tokio, or rustls-platform-verifier"
382 );
383 }
384
385 #[allow(unreachable_code)]
386 {
387 panic!("Failed to build HTTPS connector: no verifier succeeded")
388 }
389}
390
391pub(crate) fn build_hyper_client() -> HyperClient {
392 let connector = build_https_connector();
393 Client::builder(TokioExecutor::new()).build(connector)
394}
395
396#[allow(dead_code)]
397pub(crate) fn build_hyper_client_with_timeout(_timeout: Duration) -> HyperClient {
398 build_hyper_client()
400}
401
402fn install_crypto_provider() {
403 #[cfg(feature = "aws-lc-rs")]
404 {
405 let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
406 }
407 #[cfg(all(feature = "ring", not(feature = "aws-lc-rs")))]
408 {
409 let _ = rustls::crypto::ring::default_provider().install_default();
410 }
411 #[cfg(not(any(feature = "aws-lc-rs", feature = "ring")))]
412 {
413 compile_error!("Either aws-lc-rs or ring feature must be enabled");
414 }
415}
416
417#[cfg(feature = "rustls-platform-verifier")]
418fn try_build_with_platform_verifier() -> Result<HttpsConnector<HttpConnector>, rustls::Error> {
419 let builder = hyper_rustls::HttpsConnectorBuilder::new().try_with_platform_verifier()?;
420 #[cfg(feature = "http2")]
421 {
422 Ok(builder
423 .https_or_http()
424 .enable_http1()
425 .enable_http2()
426 .build())
427 }
428 #[cfg(not(feature = "http2"))]
429 {
430 Ok(builder.https_or_http().enable_http1().build())
431 }
432}
433
434#[cfg(feature = "native-tokio")]
435fn try_build_with_native_roots() -> std::io::Result<HttpsConnector<HttpConnector>> {
436 let builder = hyper_rustls::HttpsConnectorBuilder::new().with_native_roots()?;
437 #[cfg(feature = "http2")]
438 {
439 Ok(builder
440 .https_or_http()
441 .enable_http1()
442 .enable_http2()
443 .build())
444 }
445 #[cfg(not(feature = "http2"))]
446 {
447 Ok(builder.https_or_http().enable_http1().build())
448 }
449}
450
451#[cfg(feature = "webpki-tokio")]
452fn build_with_webpki_roots() -> HttpsConnector<HttpConnector> {
453 let builder = hyper_rustls::HttpsConnectorBuilder::new().with_webpki_roots();
454 #[cfg(feature = "http2")]
455 {
456 builder
457 .https_or_http()
458 .enable_http1()
459 .enable_http2()
460 .build()
461 }
462 #[cfg(not(feature = "http2"))]
463 {
464 builder.https_or_http().enable_http1().build()
465 }
466}
467
468const MAX_RETRY_DELAY: Duration = Duration::from_secs(60);
469const MAX_BODY_SNIPPET: usize = 512;
470
471fn body_snippet(body: &str) -> &str {
472 let trimmed = body.trim();
473 if trimmed.len() <= MAX_BODY_SNIPPET {
474 trimmed
475 } else {
476 &trimmed[..trimmed.ceil_char_boundary(MAX_BODY_SNIPPET)]
477 }
478}
479
480fn retry_after(headers: &HeaderMap) -> Option<Duration> {
481 headers
482 .get("retry-after")?
483 .to_str()
484 .ok()?
485 .parse::<u64>()
486 .ok()
487 .map(Duration::from_secs)
488}
489
490fn http_status_message(code: u16, body: &str) -> String {
491 let trimmed = body.trim();
492 if code == 400 {
493 if trimmed.is_empty() {
494 "BadRequest".to_string()
495 } else {
496 format!("BadRequest {trimmed}")
497 }
498 } else if trimmed.is_empty() {
499 format!("HTTP {code}")
500 } else {
501 format!("HTTP {code}: {trimmed}")
502 }
503}