fhir_sdk/client/
request.rs1use std::time::Duration;
4
5use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
6use tokio_retry::{
7 RetryIf,
8 strategy::{ExponentialBackoff, FixedInterval},
9};
10
11use super::{error::Error, misc::make_uuid_header_value};
12
13#[derive(Debug, Clone)]
17pub struct RequestSettings {
18 retries: usize,
20 retry_time: Duration,
23 max_retry_time: Option<Duration>,
26 exp_backoff: bool,
28 timeout: Option<Duration>,
30 headers: HeaderMap,
32}
33
34impl Default for RequestSettings {
35 fn default() -> Self {
36 Self {
37 retries: 3,
38 retry_time: Duration::from_millis(1000),
39 max_retry_time: None,
40 exp_backoff: false,
41 timeout: None,
42 headers: HeaderMap::new(),
43 }
44 }
45}
46
47impl RequestSettings {
48 #[must_use]
50 pub const fn retries(mut self, retries: usize) -> Self {
51 self.retries = retries;
52 self
53 }
54
55 #[must_use]
57 pub const fn fixed_retry(mut self, interval: Duration) -> Self {
58 self.exp_backoff = false;
59 self.retry_time = interval;
60 self.max_retry_time = None;
61 self
62 }
63
64 #[must_use]
66 pub const fn exp_backoff(mut self, start_time: Duration, max_time: Option<Duration>) -> Self {
67 self.exp_backoff = true;
68 self.retry_time = start_time;
69 self.max_retry_time = max_time;
70 self
71 }
72
73 #[must_use]
75 pub const fn timeout(mut self, timeout: Option<Duration>) -> Self {
76 self.timeout = timeout;
77 self
78 }
79
80 #[must_use]
82 pub fn header(mut self, header: HeaderName, value: HeaderValue) -> Self {
83 self.headers.insert(header, value);
84 self
85 }
86
87 #[tracing::instrument(level = "debug", skip_all, fields(x_correlation_id, x_request_id))]
91 pub(crate) async fn make_request(
92 &self,
93 mut request: reqwest::RequestBuilder,
94 ) -> Result<reqwest::Response, Error> {
95 if let Some(timeout) = self.timeout {
96 request = request.timeout(timeout);
97 }
98
99 let (client, request_result) = request.build_split();
101 let mut request = request_result?;
102 let mut headers = self.headers.clone();
103 headers.extend(request.headers().clone());
104 *request.headers_mut() = headers;
105
106 let id_value = make_uuid_header_value();
108 request.headers_mut().entry("X-Correlation-Id").or_insert_with(|| id_value.clone());
109 request.headers_mut().entry("X-Request-Id").or_insert(id_value);
110
111 #[expect(
113 clippy::cast_possible_truncation,
114 reason = "That high backoff is unrealistic and not an issue"
115 )]
116 let strategy: Box<dyn Iterator<Item = Duration> + Send + Sync> = if self.exp_backoff {
117 let mut exp_backoff =
118 ExponentialBackoff::from_millis(self.retry_time.as_millis() as u64);
119 if let Some(max_backoff) = self.max_retry_time {
120 exp_backoff = exp_backoff.max_delay(max_backoff);
121 }
122 Box::new(exp_backoff)
123 } else {
124 Box::new(FixedInterval::from_millis(self.retry_time.as_millis() as u64))
125 };
126
127 let x_correlation_id =
129 request.headers().get("X-Correlation-Id").and_then(|v| v.to_str().ok());
130 let x_request_id = request.headers().get("X-Request-Id").and_then(|v| v.to_str().ok());
131 tracing::Span::current()
132 .record("x_correlation_id", x_correlation_id)
133 .record("x_request_id", x_request_id);
134
135 RetryIf::spawn(
137 strategy.take(self.retries),
138 async || {
139 tracing::debug!("Sending {} request to {}", request.method(), request.url());
140 let request = request.try_clone().ok_or(Error::RequestNotClone)?;
141 let response = client.execute(request).await?;
142 tracing::debug!("Got response: {}", response.status());
143 Ok(response)
144 },
145 Error::should_retry,
146 )
147 .await
148 }
149}