1use std::{
2 collections::BTreeMap,
3 io::Write,
4 path::Path,
5 sync::{Arc, OnceLock, RwLock},
6 time::Duration,
7};
8
9use bytes::Bytes;
10use reqwest::{Method, StatusCode, header};
11use serde::{Serialize, de::DeserializeOwned};
12use serde_json::Value;
13use tokio::time;
14
15use super::{AuthInjector, Error};
16use crate::{CliCoreError, Result};
17
18const MAX_RETRIES: usize = 3;
19const BASE_BACKOFF: Duration = Duration::from_millis(500);
20const BUILTIN_DEFAULT_USER_AGENT: &str = "cli/dev";
21static DEFAULT_USER_AGENT: OnceLock<RwLock<String>> = OnceLock::new();
22
23pub fn set_default_user_agent(user_agent: impl Into<String>) {
31 let lock =
32 DEFAULT_USER_AGENT.get_or_init(|| RwLock::new(BUILTIN_DEFAULT_USER_AGENT.to_owned()));
33 if let Ok(mut current) = lock.write() {
34 *current = user_agent.into();
35 }
36}
37
38pub(crate) fn default_user_agent() -> String {
44 DEFAULT_USER_AGENT
45 .get_or_init(|| RwLock::new(BUILTIN_DEFAULT_USER_AGENT.to_owned()))
46 .read()
47 .map_or_else(
48 |_| BUILTIN_DEFAULT_USER_AGENT.to_owned(),
49 |value| value.clone(),
50 )
51}
52
53#[cfg(test)]
57pub(crate) static UA_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
58
59#[cfg(test)]
64pub(crate) struct RestoreDefaultUserAgent;
65
66#[cfg(test)]
67impl Drop for RestoreDefaultUserAgent {
68 fn drop(&mut self) {
69 set_default_user_agent(BUILTIN_DEFAULT_USER_AGENT);
70 }
71}
72
73static DEFAULT_TRANSPORT_LOGGER: OnceLock<RwLock<Arc<dyn TransportLogger>>> = OnceLock::new();
74
75fn default_transport_logger_lock() -> &'static RwLock<Arc<dyn TransportLogger>> {
76 DEFAULT_TRANSPORT_LOGGER.get_or_init(|| RwLock::new(Arc::new(NoopTransportLogger)))
77}
78
79pub fn set_default_transport_logger(logger: Arc<dyn TransportLogger>) {
91 let mut current = default_transport_logger_lock()
95 .write()
96 .unwrap_or_else(std::sync::PoisonError::into_inner);
97 *current = logger;
98}
99
100#[must_use]
104pub fn default_transport_logger() -> Arc<dyn TransportLogger> {
105 default_transport_logger_lock()
106 .read()
107 .unwrap_or_else(std::sync::PoisonError::into_inner)
108 .clone()
109}
110
111pub fn debug_log_reqwest_request(request: &reqwest::Request) {
120 let logger = default_transport_logger();
121 if !logger.enabled() {
122 return;
123 }
124 logger.debug(&TransportLogEvent {
125 message: "http request",
126 fields: BTreeMap::from([
127 ("method".to_owned(), request.method().as_str().to_owned()),
128 ("url".to_owned(), request.url().as_str().to_owned()),
129 ]),
130 headers: Some(header_pairs(request.headers())),
131 body: request
132 .body()
133 .and_then(reqwest::Body::as_bytes)
134 .map(<[u8]>::to_vec),
135 });
136}
137
138pub fn debug_log_reqwest_response(status: StatusCode, headers: &header::HeaderMap, body: &[u8]) {
146 let logger = default_transport_logger();
147 if !logger.enabled() {
148 return;
149 }
150 logger.debug(&TransportLogEvent {
151 message: "http response",
152 fields: BTreeMap::from([("status".to_owned(), status.as_u16().to_string())]),
153 headers: Some(header_pairs(headers)),
154 body: Some(body.to_vec()),
155 });
156}
157
158#[derive(serde::Deserialize)]
159struct GraphQlError {
160 message: String,
161}
162
163#[derive(Default, serde::Deserialize)]
164struct GraphQlEnvelope {
165 data: Option<Value>,
166 #[serde(default)]
167 errors: Vec<GraphQlError>,
168}
169
170#[derive(Clone, Debug, Default)]
178pub struct TransportLogEvent {
179 pub message: &'static str,
181 pub fields: BTreeMap<String, String>,
183 pub headers: Option<Vec<(String, String)>>,
185 pub body: Option<Vec<u8>>,
189}
190
191pub trait TransportLogger: Send + Sync + std::fmt::Debug {
193 fn debug(&self, event: &TransportLogEvent);
195
196 fn enabled(&self) -> bool {
203 true
204 }
205}
206
207#[derive(Clone, Debug, Default)]
209pub struct NoopTransportLogger;
210
211impl TransportLogger for NoopTransportLogger {
212 fn debug(&self, _event: &TransportLogEvent) {}
213
214 fn enabled(&self) -> bool {
215 false
216 }
217}
218
219#[derive(Clone, Debug)]
226pub struct HttpClient {
227 base: reqwest::Client,
228 base_url: String,
229 auth: Arc<dyn AuthInjector>,
230 user_agent: String,
231 default_headers: BTreeMap<String, String>,
232 logger: Arc<dyn TransportLogger>,
233}
234
235#[derive(Clone, Debug)]
237pub struct HttpClientBuilder {
238 base_url: String,
239 auth: Arc<dyn AuthInjector>,
240 user_agent: String,
241 default_headers: BTreeMap<String, String>,
242 logger: Arc<dyn TransportLogger>,
243}
244
245impl HttpClientBuilder {
246 #[must_use]
248 pub fn new(base_url: impl Into<String>, auth: Arc<dyn AuthInjector>) -> Self {
249 Self {
250 base_url: base_url.into(),
251 auth,
252 user_agent: default_user_agent(),
253 default_headers: BTreeMap::new(),
254 logger: default_transport_logger(),
255 }
256 }
257
258 #[must_use]
260 pub fn user_agent(mut self, user_agent: impl Into<String>) -> Self {
261 self.user_agent = user_agent.into();
262 self
263 }
264
265 #[must_use]
267 pub fn with_user_agent(self, user_agent: impl Into<String>) -> Self {
268 self.user_agent(user_agent)
269 }
270
271 #[must_use]
273 pub fn default_headers(mut self, headers: BTreeMap<String, String>) -> Self {
274 self.default_headers = headers;
275 self
276 }
277
278 #[must_use]
280 pub fn with_default_headers(self, headers: BTreeMap<String, String>) -> Self {
281 self.default_headers(headers)
282 }
283
284 #[must_use]
286 pub fn logger(mut self, logger: Arc<dyn TransportLogger>) -> Self {
287 self.logger = logger;
288 self
289 }
290
291 #[must_use]
293 pub fn with_logger(self, logger: Arc<dyn TransportLogger>) -> Self {
294 self.logger(logger)
295 }
296
297 #[must_use]
299 pub fn build(self) -> HttpClient {
300 HttpClient {
301 base: reqwest::Client::new(),
302 base_url: self.base_url,
303 auth: self.auth,
304 user_agent: self.user_agent,
305 default_headers: self.default_headers,
306 logger: self.logger,
307 }
308 }
309}
310
311impl HttpClient {
312 #[must_use]
314 pub fn builder(base_url: impl Into<String>, auth: Arc<dyn AuthInjector>) -> HttpClientBuilder {
315 HttpClientBuilder::new(base_url, auth)
316 }
317
318 #[must_use]
320 pub fn new(base_url: impl Into<String>, auth: Arc<dyn AuthInjector>) -> Self {
321 HttpClientBuilder::new(base_url, auth).build()
322 }
323
324 pub async fn get<T: Default + DeserializeOwned>(&self, path: &str) -> Result<T> {
326 self.do_json(Method::GET, path, Option::<&()>::None).await
327 }
328
329 pub async fn get_without_response(&self, path: &str) -> Result<()> {
331 self.do_empty(Method::GET, path, Option::<&()>::None).await
332 }
333
334 pub async fn post<B: Serialize, T: Default + DeserializeOwned>(
336 &self,
337 path: &str,
338 body: &B,
339 ) -> Result<T> {
340 self.do_json(Method::POST, path, Some(body)).await
341 }
342
343 pub async fn post_without_response<B: Serialize>(&self, path: &str, body: &B) -> Result<()> {
345 self.do_empty(Method::POST, path, Some(body)).await
346 }
347
348 pub async fn put<B: Serialize, T: Default + DeserializeOwned>(
350 &self,
351 path: &str,
352 body: &B,
353 ) -> Result<T> {
354 self.do_json(Method::PUT, path, Some(body)).await
355 }
356
357 pub async fn put_without_response<B: Serialize>(&self, path: &str, body: &B) -> Result<()> {
359 self.do_empty(Method::PUT, path, Some(body)).await
360 }
361
362 pub async fn patch<B: Serialize, T: Default + DeserializeOwned>(
364 &self,
365 path: &str,
366 body: &B,
367 ) -> Result<T> {
368 self.do_json(Method::PATCH, path, Some(body)).await
369 }
370
371 pub async fn patch_without_response<B: Serialize>(&self, path: &str, body: &B) -> Result<()> {
373 self.do_empty(Method::PATCH, path, Some(body)).await
374 }
375
376 pub async fn delete(&self, path: &str) -> Result<()> {
378 self.do_empty(Method::DELETE, path, Option::<&()>::None)
379 .await
380 }
381
382 pub async fn delete_with_body<B: Serialize>(&self, path: &str, body: &B) -> Result<()> {
384 self.do_empty(Method::DELETE, path, Some(body)).await
385 }
386
387 pub async fn get_etag<T: Default + DeserializeOwned>(&self, path: &str) -> Result<(T, String)> {
389 let response = self.send_get_status_only_retry(path).await?;
390 let etag = response
391 .headers()
392 .get(header::ETAG)
393 .and_then(|value| value.to_str().ok())
394 .unwrap_or_default()
395 .to_owned();
396 let value = self.decode_json_response(response, "GET", path).await?;
397 Ok((value, etag))
398 }
399
400 pub async fn get_etag_without_response(&self, path: &str) -> Result<String> {
402 let response = self.send_get_status_only_retry(path).await?;
403 let etag = response
404 .headers()
405 .get(header::ETAG)
406 .and_then(|value| value.to_str().ok())
407 .unwrap_or_default()
408 .to_owned();
409 self.ensure_success_response(response, "GET", path).await?;
410 Ok(etag)
411 }
412
413 pub async fn put_if_match<B: Serialize, T: Default + DeserializeOwned>(
415 &self,
416 path: &str,
417 body: &B,
418 etag: &str,
419 ) -> Result<T> {
420 let response = self.send_put_if_match(path, body, etag).await?;
421 self.decode_json_response(response, "PUT", path).await
422 }
423
424 pub async fn put_if_match_without_response<B: Serialize>(
426 &self,
427 path: &str,
428 body: &B,
429 etag: &str,
430 ) -> Result<()> {
431 let response = self.send_put_if_match(path, body, etag).await?;
432 self.ensure_success_response(response, "PUT", path).await
433 }
434
435 pub async fn get_raw(&self, path: &str, writer: &mut dyn Write) -> Result<()> {
437 let response = self.send_get_raw_status_only_retry(path).await?;
438 let (status, bytes) = self
439 .read_and_log_response(response, "GET", path, false)
440 .await?;
441 if status.is_client_error() || status.is_server_error() {
442 return Err(
443 parse_error_body(status, &String::from_utf8_lossy(&bytes), "GET", path).into(),
444 );
445 }
446 writer.write_all(&bytes)?;
447 Ok(())
448 }
449
450 pub async fn get_bytes(&self, path: &str) -> Result<Vec<u8>> {
452 let response = self.send_get_raw_status_only_retry(path).await?;
453 let (status, bytes) = self
454 .read_and_log_response(response, "GET", path, false)
455 .await?;
456 if status.is_client_error() || status.is_server_error() {
457 return Err(
458 parse_error_body(status, &String::from_utf8_lossy(&bytes), "GET", path).into(),
459 );
460 }
461 Ok(bytes.to_vec())
462 }
463
464 pub async fn post_raw<B: Serialize>(
466 &self,
467 path: &str,
468 body: Option<&B>,
469 writer: &mut dyn Write,
470 ) -> Result<()> {
471 let response = self.send_post_raw_once(path, body).await?;
472 let (status, bytes) = self
473 .read_and_log_response(response, "POST", path, false)
474 .await?;
475 if status.is_client_error() || status.is_server_error() {
476 return Err(
477 parse_error_body(status, &String::from_utf8_lossy(&bytes), "POST", path).into(),
478 );
479 }
480 writer.write_all(&bytes)?;
481 Ok(())
482 }
483
484 pub async fn do_raw<T: Default + DeserializeOwned>(
486 &self,
487 method: Method,
488 path: &str,
489 content_type: &str,
490 body: impl Into<Vec<u8>>,
491 ) -> Result<T> {
492 self.do_raw_optional_body(method, path, content_type, Some(body.into()))
493 .await
494 }
495
496 pub async fn do_raw_optional_body<T: Default + DeserializeOwned>(
498 &self,
499 method: Method,
500 path: &str,
501 content_type: &str,
502 body: Option<Vec<u8>>,
503 ) -> Result<T> {
504 let method_text = method.as_str().to_owned();
505 let response = self.send_raw_once(method, path, content_type, body).await?;
506 self.decode_json_response(response, &method_text, path)
507 .await
508 }
509
510 pub async fn do_raw_without_response(
512 &self,
513 method: Method,
514 path: &str,
515 content_type: &str,
516 body: impl Into<Vec<u8>>,
517 ) -> Result<()> {
518 self.do_raw_optional_body_without_response(method, path, content_type, Some(body.into()))
519 .await
520 }
521
522 pub async fn do_raw_optional_body_without_response(
524 &self,
525 method: Method,
526 path: &str,
527 content_type: &str,
528 body: Option<Vec<u8>>,
529 ) -> Result<()> {
530 let method_text = method.as_str().to_owned();
531 let response = self.send_raw_once(method, path, content_type, body).await?;
532 self.ensure_success_response(response, &method_text, path)
533 .await
534 }
535
536 pub async fn post_multipart<T: Default + DeserializeOwned>(
538 &self,
539 path: &str,
540 field_name: &str,
541 file_path: &Path,
542 ) -> Result<T> {
543 self.post_multipart_with_fields(path, field_name, file_path, &BTreeMap::new())
544 .await
545 }
546
547 pub async fn post_multipart_without_response(
549 &self,
550 path: &str,
551 field_name: &str,
552 file_path: &Path,
553 ) -> Result<()> {
554 self.post_multipart_with_fields_without_response(
555 path,
556 field_name,
557 file_path,
558 &BTreeMap::new(),
559 )
560 .await
561 }
562
563 pub async fn post_multipart_with_fields<T: Default + DeserializeOwned>(
565 &self,
566 path: &str,
567 file_field: &str,
568 file_path: &Path,
569 fields: &BTreeMap<String, String>,
570 ) -> Result<T> {
571 let form = self.multipart_form(file_field, file_path, fields).await?;
572 self.send_multipart(path, form).await
573 }
574
575 async fn multipart_form(
576 &self,
577 file_field: &str,
578 file_path: &Path,
579 fields: &BTreeMap<String, String>,
580 ) -> Result<reqwest::multipart::Form> {
581 let mut form = reqwest::multipart::Form::new();
582 for (key, value) in fields {
583 form = form.text(key.clone(), value.clone());
584 }
585 let file_name = file_path
586 .file_name()
587 .and_then(|name| name.to_str())
588 .unwrap_or("file")
589 .to_owned();
590 let bytes = tokio::fs::read(file_path)
591 .await
592 .map_err(|err| CliCoreError::message(format!("transport: open file: {err}")))?;
593 let part = reqwest::multipart::Part::bytes(bytes).file_name(file_name);
594 form = form.part(file_field.to_owned(), part);
595 Ok(form)
596 }
597
598 pub async fn post_multipart_with_fields_without_response(
600 &self,
601 path: &str,
602 file_field: &str,
603 file_path: &Path,
604 fields: &BTreeMap<String, String>,
605 ) -> Result<()> {
606 let form = self.multipart_form(file_field, file_path, fields).await?;
607 self.send_multipart_without_response(path, form).await
608 }
609
610 pub async fn post_multipart_fields<T: Default + DeserializeOwned>(
612 &self,
613 path: &str,
614 fields: &BTreeMap<String, String>,
615 ) -> Result<T> {
616 let mut form = reqwest::multipart::Form::new();
617 for (key, value) in fields {
618 form = form.text(key.clone(), value.clone());
619 }
620 self.send_multipart(path, form).await
621 }
622
623 pub async fn post_multipart_fields_without_response(
625 &self,
626 path: &str,
627 fields: &BTreeMap<String, String>,
628 ) -> Result<()> {
629 let mut form = reqwest::multipart::Form::new();
630 for (key, value) in fields {
631 form = form.text(key.clone(), value.clone());
632 }
633 self.send_multipart_without_response(path, form).await
634 }
635
636 pub async fn post_graphql<T: DeserializeOwned + Default>(
638 &self,
639 path: &str,
640 query: &str,
641 variables: BTreeMap<String, Value>,
642 ) -> Result<T> {
643 self.post_graphql_optional_variables(path, query, Some(variables))
644 .await
645 }
646
647 pub async fn post_graphql_optional_variables<T: DeserializeOwned + Default>(
649 &self,
650 path: &str,
651 query: &str,
652 variables: Option<BTreeMap<String, Value>>,
653 ) -> Result<T> {
654 let mut result = T::default();
655 self.post_graphql_optional_variables_into(path, query, variables, &mut result)
656 .await?;
657 Ok(result)
658 }
659
660 pub async fn post_graphql_without_response(
662 &self,
663 path: &str,
664 query: &str,
665 variables: BTreeMap<String, Value>,
666 ) -> Result<()> {
667 self.post_graphql_optional_variables_without_response(path, query, Some(variables))
668 .await
669 }
670
671 pub async fn post_graphql_optional_variables_without_response(
673 &self,
674 path: &str,
675 query: &str,
676 variables: Option<BTreeMap<String, Value>>,
677 ) -> Result<()> {
678 self.post_graphql_response_envelope(path, query, variables)
679 .await?;
680 Ok(())
681 }
682
683 pub async fn post_graphql_into<T: DeserializeOwned>(
685 &self,
686 path: &str,
687 query: &str,
688 variables: BTreeMap<String, Value>,
689 result: &mut T,
690 ) -> Result<()> {
691 self.post_graphql_optional_variables_into(path, query, Some(variables), result)
692 .await
693 }
694
695 pub async fn post_graphql_optional_variables_into<T: DeserializeOwned>(
697 &self,
698 path: &str,
699 query: &str,
700 variables: Option<BTreeMap<String, Value>>,
701 result: &mut T,
702 ) -> Result<()> {
703 let envelope = self
704 .post_graphql_response_envelope(path, query, variables)
705 .await?;
706 if let Some(data) = envelope.data
707 && !data.is_null()
708 {
709 *result = serde_json::from_value(data).map_err(|err| {
710 CliCoreError::message(format!("transport: decode graphql data: {err}"))
711 })?;
712 }
713 Ok(())
714 }
715
716 async fn do_json<B: Serialize, T: Default + DeserializeOwned>(
717 &self,
718 method: Method,
719 path: &str,
720 body: Option<&B>,
721 ) -> Result<T> {
722 let method_text = method.as_str().to_owned();
723 let response = self.send_with_retry(method, path, body).await?;
724 self.decode_json_response(response, &method_text, path)
725 .await
726 }
727
728 async fn post_graphql_response_envelope(
729 &self,
730 path: &str,
731 query: &str,
732 variables: Option<BTreeMap<String, Value>>,
733 ) -> Result<GraphQlEnvelope> {
734 #[derive(Serialize)]
735 struct Request<'query> {
736 query: &'query str,
737 variables: Option<BTreeMap<String, Value>>,
738 }
739
740 let envelope: GraphQlEnvelope = self.post(path, &Request { query, variables }).await?;
741 if !envelope.errors.is_empty() {
742 let message = envelope
743 .errors
744 .iter()
745 .map(|error| error.message.as_str())
746 .collect::<Vec<_>>()
747 .join("; ");
748 return Err(CliCoreError::message(format!("graphql: {message}")));
749 }
750 Ok(envelope)
751 }
752
753 async fn send_put_if_match<B: Serialize>(
754 &self,
755 path: &str,
756 body: &B,
757 etag: &str,
758 ) -> Result<reqwest::Response> {
759 let mut request = self
760 .build_request(Method::PUT, path, Some(body))?
761 .header(header::IF_MATCH, etag)
762 .build()
763 .map_err(|err| CliCoreError::message(format!("transport: create request: {err}")))?;
764 self.inject_auth(&mut request).await?;
765 self.log_request(&request);
766 self.base
767 .execute(request)
768 .await
769 .map_err(|err| CliCoreError::message(format!("transport: PUT {path}: {err}")))
770 }
771
772 async fn send_multipart<T: Default + DeserializeOwned>(
773 &self,
774 path: &str,
775 form: reqwest::multipart::Form,
776 ) -> Result<T> {
777 let response = self.send_multipart_response(path, form).await?;
778 self.decode_json_response(response, "POST", path).await
779 }
780
781 async fn send_multipart_without_response(
782 &self,
783 path: &str,
784 form: reqwest::multipart::Form,
785 ) -> Result<()> {
786 let response = self.send_multipart_response(path, form).await?;
787 self.ensure_success_response(response, "POST", path).await
788 }
789
790 async fn send_multipart_response(
791 &self,
792 path: &str,
793 form: reqwest::multipart::Form,
794 ) -> Result<reqwest::Response> {
795 let url = format!("{}{}", self.base_url, path);
796 let mut builder = self
797 .base
798 .post(url)
799 .header(header::USER_AGENT, self.user_agent.clone())
800 .multipart(form);
801 for (key, value) in &self.default_headers {
802 builder = builder.header(key, value);
803 }
804 let mut request = builder
805 .build()
806 .map_err(|err| CliCoreError::message(format!("transport: create request: {err}")))?;
807 self.inject_auth(&mut request).await?;
808 self.log_request(&request);
809 self.base
810 .execute(request)
811 .await
812 .map_err(|err| CliCoreError::message(format!("transport: POST {path}: {err}")))
813 }
814
815 async fn do_empty<B: Serialize>(
816 &self,
817 method: Method,
818 path: &str,
819 body: Option<&B>,
820 ) -> Result<()> {
821 let method_text = method.as_str().to_owned();
822 let response = self.send_with_retry(method, path, body).await?;
823 self.ensure_success_response(response, &method_text, path)
824 .await
825 }
826
827 async fn send_raw_once(
828 &self,
829 method: Method,
830 path: &str,
831 content_type: &str,
832 body: Option<Vec<u8>>,
833 ) -> Result<reqwest::Response> {
834 let url = format!("{}{}", self.base_url, path);
835 let method_text = method.as_str().to_owned();
836 let mut builder = self
837 .base
838 .request(method, url)
839 .header(header::USER_AGENT, self.user_agent.clone());
840 if let Some(body) = body {
841 builder = builder.body(body);
842 }
843 if !content_type.is_empty() {
844 builder = builder.header(header::CONTENT_TYPE, content_type);
845 }
846 for (key, value) in &self.default_headers {
847 builder = builder.header(key, value);
848 }
849 let mut request = builder
850 .build()
851 .map_err(|err| CliCoreError::message(format!("transport: create request: {err}")))?;
852 self.inject_auth(&mut request).await?;
853 self.log_request(&request);
854 self.base
855 .execute(request)
856 .await
857 .map_err(|err| CliCoreError::message(format!("transport: {method_text} {path}: {err}")))
858 }
859
860 async fn send_get_raw_status_only_retry(&self, path: &str) -> Result<reqwest::Response> {
861 let mut last_err = None;
862 for attempt in 0..MAX_RETRIES {
863 if attempt > 0 {
864 let backoff = BASE_BACKOFF * 2_u32.pow(u32::try_from(attempt - 1).unwrap_or(0));
865 time::sleep(backoff).await;
866 }
867
868 match self.send_get_raw_once(path).await {
869 Ok(response) => {
870 let status = response.status();
871 if status == StatusCode::TOO_MANY_REQUESTS || status.is_server_error() {
872 self.log_response("GET", path, status, response.headers(), None, None);
873 last_err = Some(CliCoreError::message(format!(
874 "transport: GET {}: status {}",
875 path,
876 status.as_u16()
877 )));
878 continue;
879 }
880 return Ok(response);
881 }
882 Err(err) => last_err = Some(err),
883 }
884 }
885 Err(last_err.unwrap_or_else(|| CliCoreError::message("transport: retry failed")))
886 }
887
888 async fn send_get_raw_once(&self, path: &str) -> Result<reqwest::Response> {
889 let url = format!("{}{}", self.base_url, path);
890 let mut builder = self
891 .base
892 .get(url)
893 .header(header::USER_AGENT, self.user_agent.clone());
894 for (key, value) in &self.default_headers {
895 builder = builder.header(key, value);
896 }
897 let mut request = builder
898 .build()
899 .map_err(|err| CliCoreError::message(format!("transport: create request: {err}")))?;
900 self.inject_auth(&mut request).await?;
901 self.log_request(&request);
902 self.base
903 .execute(request)
904 .await
905 .map_err(|err| CliCoreError::message(format!("transport: GET {path}: {err}")))
906 }
907
908 async fn send_post_raw_once<B: Serialize>(
909 &self,
910 path: &str,
911 body: Option<&B>,
912 ) -> Result<reqwest::Response> {
913 let mut request = self
914 .build_request(Method::POST, path, body)?
915 .build()
916 .map_err(|err| CliCoreError::message(format!("transport: create request: {err}")))?;
917 self.inject_auth(&mut request).await?;
918 self.log_request(&request);
919 self.base
920 .execute(request)
921 .await
922 .map_err(|err| CliCoreError::message(format!("transport: POST {path}: {err}")))
923 }
924
925 async fn send_with_retry<B: Serialize>(
926 &self,
927 method: Method,
928 path: &str,
929 body: Option<&B>,
930 ) -> Result<reqwest::Response> {
931 let mut last_err = None;
932 for attempt in 0..MAX_RETRIES {
933 if attempt > 0 {
934 let backoff = BASE_BACKOFF * 2_u32.pow(u32::try_from(attempt - 1).unwrap_or(0));
935 self.log_debug(
936 "retrying request",
937 [
938 ("attempt", (attempt + 1).to_string()),
939 ("backoff", format!("{backoff:?}")),
940 ],
941 );
942 time::sleep(backoff).await;
943 }
944
945 match self.send_once(method.clone(), path, body).await {
946 Ok(response) => {
947 if retryable_status(method.clone(), response.status()) {
948 last_err = Some(
949 self.retryable_status_error(response, method.as_str(), path)
950 .await,
951 );
952 continue;
953 }
954 return Ok(response);
955 }
956 Err(err) if is_idempotent(&method) => {
957 last_err = Some(err);
958 }
959 Err(err) => return Err(err),
960 }
961 }
962 Err(last_err.unwrap_or_else(|| CliCoreError::message("transport: retry failed")))
963 }
964
965 async fn send_get_status_only_retry(&self, path: &str) -> Result<reqwest::Response> {
966 let mut last_err = None;
967 for attempt in 0..MAX_RETRIES {
968 if attempt > 0 {
969 let backoff = BASE_BACKOFF * 2_u32.pow(u32::try_from(attempt - 1).unwrap_or(0));
970 self.log_debug(
971 "retrying request",
972 [
973 ("attempt", (attempt + 1).to_string()),
974 ("backoff", format!("{backoff:?}")),
975 ],
976 );
977 time::sleep(backoff).await;
978 }
979
980 match self.send_once(Method::GET, path, Option::<&()>::None).await {
981 Ok(response) => {
982 let status = response.status();
983 if status == StatusCode::TOO_MANY_REQUESTS || status.is_server_error() {
984 self.log_response("GET", path, status, response.headers(), None, None);
985 last_err = Some(CliCoreError::message(format!(
986 "transport: GET {}: status {}",
987 path,
988 status.as_u16()
989 )));
990 continue;
991 }
992 return Ok(response);
993 }
994 Err(err) => last_err = Some(err),
995 }
996 }
997 Err(last_err.unwrap_or_else(|| CliCoreError::message("transport: retry failed")))
998 }
999
1000 async fn send_once<B: Serialize>(
1001 &self,
1002 method: Method,
1003 path: &str,
1004 body: Option<&B>,
1005 ) -> Result<reqwest::Response> {
1006 let mut request = self
1007 .build_request(method.clone(), path, body)?
1008 .build()
1009 .map_err(|err| CliCoreError::message(format!("transport: create request: {err}")))?;
1010 self.inject_auth(&mut request).await?;
1011 let method_text = method.as_str().to_owned();
1012 self.log_request(&request);
1013 self.base
1014 .execute(request)
1015 .await
1016 .map_err(|err| CliCoreError::message(format!("transport: {method_text} {path}: {err}")))
1017 }
1018
1019 fn build_request<B: Serialize>(
1020 &self,
1021 method: Method,
1022 path: &str,
1023 body: Option<&B>,
1024 ) -> Result<reqwest::RequestBuilder> {
1025 let url = format!("{}{}", self.base_url, path);
1026 let mut builder = self
1027 .base
1028 .request(method, url)
1029 .header(header::USER_AGENT, self.user_agent.clone());
1030 if let Some(body) = body {
1031 let body = serde_json::to_vec(body)
1032 .map_err(|err| CliCoreError::message(format!("transport: marshal body: {err}")))?;
1033 builder = builder
1034 .header(header::CONTENT_TYPE, "application/json")
1035 .body(body);
1036 }
1037 for (key, value) in &self.default_headers {
1038 builder = builder.header(key, value);
1039 }
1040 Ok(builder)
1041 }
1042
1043 fn log_debug(
1044 &self,
1045 message: &'static str,
1046 fields: impl IntoIterator<Item = (&'static str, String)>,
1047 ) {
1048 if !self.logger.enabled() {
1049 return;
1050 }
1051 self.logger.debug(&TransportLogEvent {
1052 message,
1053 fields: fields
1054 .into_iter()
1055 .map(|(key, value)| (key.to_owned(), value))
1056 .collect(),
1057 headers: None,
1058 body: None,
1059 });
1060 }
1061
1062 fn log_request(&self, request: &reqwest::Request) {
1068 if !self.logger.enabled() {
1069 return;
1070 }
1071 self.logger.debug(&TransportLogEvent {
1072 message: "http request",
1073 fields: BTreeMap::from([
1074 ("method".to_owned(), request.method().as_str().to_owned()),
1075 ("url".to_owned(), request.url().as_str().to_owned()),
1076 ]),
1077 headers: Some(header_pairs(request.headers())),
1078 body: request
1079 .body()
1080 .and_then(reqwest::Body::as_bytes)
1081 .map(<[u8]>::to_vec),
1082 });
1083 }
1084
1085 fn log_response(
1089 &self,
1090 method: &str,
1091 path: &str,
1092 status: StatusCode,
1093 headers: &header::HeaderMap,
1094 body: Option<&[u8]>,
1095 body_bytes: Option<usize>,
1096 ) {
1097 if !self.logger.enabled() {
1098 return;
1099 }
1100 let mut fields = BTreeMap::from([
1101 ("status".to_owned(), status.as_u16().to_string()),
1102 ("method".to_owned(), method.to_owned()),
1103 ("url".to_owned(), format!("{}{}", self.base_url, path)),
1104 ]);
1105 if let Some(len) = body_bytes {
1106 fields.insert("body_bytes".to_owned(), len.to_string());
1107 }
1108 self.logger.debug(&TransportLogEvent {
1109 message: "http response",
1110 fields,
1111 headers: Some(header_pairs(headers)),
1112 body: body.map(<[u8]>::to_vec),
1113 });
1114 }
1115
1116 async fn read_and_log_response(
1124 &self,
1125 response: reqwest::Response,
1126 method: &str,
1127 path: &str,
1128 include_body: bool,
1129 ) -> Result<(StatusCode, Bytes)> {
1130 let status = response.status();
1131 let logging = self.logger.enabled();
1132 let headers = logging.then(|| response.headers().clone());
1133 let body = response.bytes().await.map_err(|err| {
1134 CliCoreError::message(format!("transport: read response body: {err}"))
1135 })?;
1136 if let Some(headers) = headers {
1137 if include_body {
1138 self.log_response(method, path, status, &headers, Some(&body), None);
1139 } else {
1140 self.log_response(method, path, status, &headers, None, Some(body.len()));
1141 }
1142 }
1143 Ok((status, body))
1144 }
1145
1146 async fn inject_auth(&self, request: &mut reqwest::Request) -> Result<()> {
1147 self.auth
1148 .inject(request)
1149 .await
1150 .map_err(|err| CliCoreError::message(format!("transport: auth inject: {err}")))
1151 }
1152
1153 async fn decode_json_response<T: Default + DeserializeOwned>(
1154 &self,
1155 response: reqwest::Response,
1156 method: &str,
1157 path: &str,
1158 ) -> Result<T> {
1159 let (status, body) = self
1160 .read_and_log_response(response, method, path, true)
1161 .await?;
1162 if status.is_client_error() || status.is_server_error() {
1163 return Err(
1164 parse_error_body(status, &String::from_utf8_lossy(&body), method, path).into(),
1165 );
1166 }
1167 if status == StatusCode::NO_CONTENT {
1168 return Ok(T::default());
1169 }
1170 if body.trim_ascii() == b"null" {
1171 return Ok(T::default());
1172 }
1173 serde_json::from_slice::<T>(&body)
1174 .map_err(|err| CliCoreError::message(format!("transport: decode response: {err}")))
1175 }
1176
1177 async fn ensure_success_response(
1178 &self,
1179 response: reqwest::Response,
1180 method: &str,
1181 path: &str,
1182 ) -> Result<()> {
1183 let is_error = response.status().is_client_error() || response.status().is_server_error();
1188 if !is_error && !self.logger.enabled() {
1189 return Ok(());
1190 }
1191 let (status, body) = self
1192 .read_and_log_response(response, method, path, true)
1193 .await?;
1194 if status.is_client_error() || status.is_server_error() {
1195 return Err(
1196 parse_error_body(status, &String::from_utf8_lossy(&body), method, path).into(),
1197 );
1198 }
1199 Ok(())
1200 }
1201
1202 async fn retryable_status_error(
1203 &self,
1204 response: reqwest::Response,
1205 method: &str,
1206 path: &str,
1207 ) -> CliCoreError {
1208 let status = response.status();
1209 let headers = self.logger.enabled().then(|| response.headers().clone());
1210 match response.bytes().await {
1211 Ok(body) => {
1212 if let Some(headers) = &headers {
1213 self.log_response(method, path, status, headers, Some(&body), None);
1214 }
1215 CliCoreError::message(format!(
1216 "transport: {method} {path}: status {}: {}",
1217 status.as_u16(),
1218 String::from_utf8_lossy(&body)
1219 ))
1220 }
1221 Err(err) => {
1222 if let Some(headers) = &headers {
1223 self.log_response(method, path, status, headers, None, None);
1224 }
1225 CliCoreError::message(format!(
1226 "transport: {method} {path}: status {} (body read failed: {err})",
1227 status.as_u16()
1228 ))
1229 }
1230 }
1231 }
1232}
1233
1234fn header_pairs(headers: &header::HeaderMap) -> Vec<(String, String)> {
1239 headers
1240 .iter()
1241 .map(|(name, value)| {
1242 let value = value.to_str().map_or_else(
1243 |_| format!("<{} non-utf8 bytes>", value.as_bytes().len()),
1244 str::to_owned,
1245 );
1246 (name.as_str().to_owned(), value)
1247 })
1248 .collect()
1249}
1250
1251pub async fn parse_error_response(response: reqwest::Response, method: &str, path: &str) -> Error {
1258 let status = response.status();
1259 let body = response.text().await.unwrap_or_default();
1260 parse_error_body(status, &body, method, path)
1261}
1262
1263fn parse_error_body(status: StatusCode, body: &str, method: &str, path: &str) -> Error {
1264 if let Ok(mut api_error) = serde_json::from_str::<Error>(body)
1265 && !api_error.message.is_empty()
1266 {
1267 api_error.code = format!("HTTP_{}", status.as_u16());
1268 return api_error;
1269 }
1270 Error {
1271 code: format!("HTTP_{}", status.as_u16()),
1272 message: format!("{} {}: {} {}", method, path, status.as_u16(), body),
1273 system: String::new(),
1274 request_id: String::new(),
1275 }
1276}
1277
1278fn retryable_status(method: Method, status: StatusCode) -> bool {
1279 status == StatusCode::TOO_MANY_REQUESTS || (status.is_server_error() && is_idempotent(&method))
1280}
1281
1282fn is_idempotent(method: &Method) -> bool {
1283 matches!(*method, Method::GET | Method::HEAD | Method::DELETE)
1284}