1use http::{HeaderMap, HeaderValue, Method};
4use serde::Serialize;
5use serde::de::DeserializeOwned;
6use tracing::Instrument as _;
7
8use crate::ModuleId;
9use crate::transport::headers::{APPLICATION_JSON, AUTHORIZATION};
10use crate::transport::{
11 CredentialsToken, OcpiError, OcpiResponse, Page, PageMeta, Quirks, RequestIds, RoutingHeaders,
12};
13use crate::types::{Url, UrlPolicy, Validate};
14
15use super::{ClientConfig, RetryPolicy};
16
17#[derive(Debug)]
19pub struct OcpiRequest {
20 pub method: Method,
22 pub url: Url,
24 pub module: ModuleId,
26 pub routing: Option<RoutingHeaders>,
28 pub ids: RequestIds,
30 pub body: Option<Vec<u8>>,
32}
33
34impl OcpiRequest {
35 #[must_use]
37 pub fn new(method: Method, url: Url, module: ModuleId) -> Self {
38 Self { method, url, module, routing: None, ids: RequestIds::generate(), body: None }
39 }
40
41 #[must_use]
45 pub fn routed(mut self, routing: RoutingHeaders) -> Self {
46 if self.module.is_functional() {
47 self.routing = Some(routing);
48 }
49 self
50 }
51
52 #[must_use]
54 pub fn with_ids(mut self, ids: RequestIds) -> Self {
55 self.ids = ids;
56 self
57 }
58
59 pub fn with_body<T: Serialize>(mut self, body: &T) -> Result<Self, OcpiError> {
65 self.body = Some(
66 serde_json::to_vec(body)
67 .map_err(|e| OcpiError::Decode { path: "/".to_owned(), message: e.to_string() })?,
68 );
69 Ok(self)
70 }
71
72 #[must_use]
80 pub fn is_retryable(&self) -> bool {
81 self.method == Method::GET
82 }
83
84 fn header_map(&self, token: &CredentialsToken, quirks: &Quirks) -> HeaderMap {
85 let mut headers = HeaderMap::new();
86 let auth = if quirks.send_unencoded_token {
87 token.to_header_value_unencoded()
88 } else {
89 token.to_header_value()
90 };
91 if let Ok(value) = HeaderValue::from_str(&auth) {
92 headers.insert(AUTHORIZATION, value);
93 }
94 self.ids.write_to(&mut headers);
95 if let Some(routing) = &self.routing
96 && !quirks.omit_routing_headers
97 {
98 routing.write_to(&mut headers);
99 }
100 if self.body.is_some() {
101 headers.insert(http::header::CONTENT_TYPE, HeaderValue::from_static(APPLICATION_JSON));
102 }
103 headers
104 }
105}
106
107#[derive(Clone, Debug)]
109pub struct Transport {
110 http: reqwest::Client,
111 config: ClientConfig,
112}
113
114impl Transport {
115 #[must_use]
117 pub fn new(http: reqwest::Client, config: ClientConfig) -> Self {
118 Self { http, config }
119 }
120
121 #[must_use]
123 pub const fn url_policy(&self) -> &UrlPolicy {
124 &self.config.url_policy
125 }
126
127 #[must_use]
129 pub const fn config(&self) -> &ClientConfig {
130 &self.config
131 }
132
133 pub async fn send<T: DeserializeOwned>(
140 &self,
141 request: &OcpiRequest,
142 token: &CredentialsToken,
143 quirks: &Quirks,
144 ) -> Result<T, OcpiError> {
145 let (response, _) = self.send_with_headers::<T>(request, token, quirks).await?;
146 response.into_result()
147 }
148
149 pub async fn send_page<T: DeserializeOwned>(
155 &self,
156 request: &OcpiRequest,
157 token: &CredentialsToken,
158 quirks: &Quirks,
159 ) -> Result<Page<T>, OcpiError> {
160 let (response, headers) = self.send_with_headers::<Vec<T>>(request, token, quirks).await?;
161 let meta = PageMeta::from_headers(&headers);
162 Ok(Page { items: response.into_list()?, meta })
163 }
164
165 pub async fn send_with_headers<T: DeserializeOwned>(
172 &self,
173 request: &OcpiRequest,
174 token: &CredentialsToken,
175 quirks: &Quirks,
176 ) -> Result<(OcpiResponse<T>, HeaderMap), OcpiError> {
177 self.config.url_policy.check(&request.url).map_err(|e| OcpiError::UrlRefused {
178 url: request.url.as_str().to_owned(),
179 reason: e.to_string(),
180 })?;
181
182 let retries = if request.is_retryable() { self.config.retry.max_attempts } else { 1 };
183 let mut attempt = 0u32;
184 loop {
185 attempt += 1;
186 match self.attempt(request, token, quirks).await {
187 Ok(result) => return Ok(result),
188 Err(error) if attempt < retries && error.is_transient() => {
189 #[cfg(feature = "client")]
190 tracing::debug!(
191 attempt,
192 %error,
193 url = request.url.as_str(),
194 "retrying a GET after a transient failure",
195 );
196 tokio::time::sleep(
197 self.config.retry.delay_for(attempt, RetryPolicy::seed_from(&request.ids.request_id)),
198 )
199 .await;
200 }
201 Err(error) => return Err(error),
202 }
203 }
204 }
205
206 async fn attempt<T: DeserializeOwned>(
207 &self,
208 request: &OcpiRequest,
209 token: &CredentialsToken,
210 quirks: &Quirks,
211 ) -> Result<(OcpiResponse<T>, HeaderMap), OcpiError> {
212 let span = tracing::info_span!(
217 "ocpi.request",
218 otel.kind = "client",
219 http.request.method = %request.method,
220 url.full = request.url.as_str(),
221 ocpi.module = %request.module,
222 ocpi.request_id = request.ids.request_id.as_str(),
223 ocpi.correlation_id = request.ids.correlation_id.as_str(),
224 ocpi.to = request.routing.as_ref().and_then(|r| r.to.as_ref()).map(ToString::to_string),
225 ocpi.from = request.routing.as_ref().map(|r| r.from.to_string()),
226 ocpi.status_code = tracing::field::Empty,
227 http.response.status_code = tracing::field::Empty,
228 );
229 self.attempt_instrumented(request, token, quirks, span.clone()).instrument(span).await
230 }
231
232 async fn attempt_instrumented<T: DeserializeOwned>(
233 &self,
234 request: &OcpiRequest,
235 token: &CredentialsToken,
236 quirks: &Quirks,
237 span: tracing::Span,
238 ) -> Result<(OcpiResponse<T>, HeaderMap), OcpiError> {
239 let mut builder = self
240 .http
241 .request(request.method.clone(), request.url.as_str())
242 .timeout(self.config.timeout)
243 .headers(request.header_map(token, quirks));
244 if let Some(body) = &request.body {
245 builder = builder.body(body.clone());
246 }
247
248 let response = builder.send().await.map_err(|e| OcpiError::Transport(strip_url(&e.to_string())))?;
249 let status = response.status();
250 let headers = response.headers().clone();
251 span.record("http.response.status_code", status.as_u16());
252
253 let bytes = response.bytes().await.map_err(|e| OcpiError::Transport(strip_url(&e.to_string())))?;
254
255 if !status.is_success() {
257 return Err(match status.as_u16() {
258 400 => OcpiError::MalformedJson(preview(&bytes)),
259 401 => OcpiError::Unauthorized(preview(&bytes)),
260 404 => OcpiError::NotFound(request.url.as_str().to_owned()),
261 405 => OcpiError::MethodNotAllowed(request.url.as_str().to_owned()),
262 other => OcpiError::Transport(format!("HTTP {other}: {}", preview(&bytes))),
263 });
264 }
265
266 let mut de = serde_json::Deserializer::from_slice(&bytes);
267 let envelope: OcpiResponse<T> = serde_path_to_error::deserialize(&mut de).map_err(|e| {
268 OcpiError::Decode { path: e.path().to_string(), message: e.into_inner().to_string() }
269 })?;
270 span.record("ocpi.status_code", envelope.status_code.get());
271 Ok((envelope, headers))
272 }
273}
274
275pub fn check_outgoing<T: Validate>(value: &T, config: &ClientConfig) -> Result<(), OcpiError> {
281 if !config.validate_outgoing {
282 return Ok(());
283 }
284 value.validate().map_err(OcpiError::Invalid)
285}
286
287fn strip_url(message: &str) -> String {
290 match message.find(" for url (") {
291 Some(at) => message[..at].to_owned(),
292 None => message.to_owned(),
293 }
294}
295
296fn preview(bytes: &[u8]) -> String {
297 let text = String::from_utf8_lossy(bytes);
298 let trimmed = text.trim();
299 if trimmed.chars().count() <= 200 {
300 return trimmed.to_owned();
301 }
302 format!("{}…", trimmed.chars().take(200).collect::<String>())
303}
304
305impl RetryPolicy {
306 #[must_use]
320 pub fn delay_for(&self, attempt: u32, seed: u64) -> std::time::Duration {
321 let exponent = attempt.saturating_sub(1).min(16);
322 let factor = 1u32 << exponent;
323 let base = self.initial_delay.saturating_mul(factor).min(self.max_delay);
324 let half = base / 2;
325 let spread = half
329 .as_nanos()
330 .try_into()
331 .map_or(0, |span: u64| if span == 0 { 0 } else { mix64(seed ^ u64::from(attempt)) % span });
332 half.saturating_add(std::time::Duration::from_nanos(spread)).min(self.max_delay)
333 }
334
335 #[must_use]
337 pub fn seed_from(request_id: &str) -> u64 {
338 let mut hash = 0xcbf2_9ce4_8422_2325_u64;
340 for byte in request_id.as_bytes() {
341 hash ^= u64::from(*byte);
342 hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
343 }
344 hash
345 }
346}
347
348const fn mix64(mut z: u64) -> u64 {
350 z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
351 z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
352 z ^ (z >> 31)
353}
354
355#[cfg(test)]
356mod tests {
357 use super::*;
358
359 #[test]
360 fn only_get_may_be_retried() {
361 let url = Url::new("https://e.com/a").unwrap();
362 let get = OcpiRequest::new(Method::GET, url.clone(), ModuleId::Cdrs);
363 let put = OcpiRequest::new(Method::PUT, url.clone(), ModuleId::Cdrs);
364 let post = OcpiRequest::new(Method::POST, url, ModuleId::Cdrs);
365 assert!(get.is_retryable());
366 assert!(!put.is_retryable(), "the spec forbids queueing and retrying writes");
367 assert!(!post.is_retryable());
368 }
369
370 #[test]
371 fn routing_headers_are_dropped_for_configuration_modules() {
372 let url = Url::new("https://e.com/a").unwrap();
373 let routing = RoutingHeaders::new(
374 crate::types::PartyRef::new("NL", "TNM").unwrap(),
375 crate::types::PartyRef::new("DE", "ABC").unwrap(),
376 );
377 let functional =
378 OcpiRequest::new(Method::GET, url.clone(), ModuleId::Locations).routed(routing.clone());
379 assert!(functional.routing.is_some());
380 let configuration = OcpiRequest::new(Method::GET, url, ModuleId::Credentials).routed(routing);
381 assert!(configuration.routing.is_none());
382 }
383
384 #[test]
385 fn the_authorization_header_follows_the_peers_quirks() {
386 let token = CredentialsToken::new("example-token").unwrap();
387 let request = OcpiRequest::new(Method::GET, Url::new("https://e.com/a").unwrap(), ModuleId::Cdrs);
388
389 let modern = request.header_map(&token, &Quirks::default());
390 assert_eq!(modern.get(AUTHORIZATION).unwrap(), "Token ZXhhbXBsZS10b2tlbg==");
391
392 let legacy = request.header_map(&token, &Quirks::for_version(&crate::VersionNumber::V2_1_1));
393 assert_eq!(legacy.get(AUTHORIZATION).unwrap(), "Token example-token");
394 }
395
396 #[test]
397 fn a_body_sets_the_content_type_and_nothing_else_does() {
398 let url = Url::new("https://e.com/a").unwrap();
399 let empty = OcpiRequest::new(Method::GET, url.clone(), ModuleId::Cdrs)
400 .header_map(&CredentialsToken::new("t").unwrap(), &Quirks::default());
401 assert!(empty.get(http::header::CONTENT_TYPE).is_none());
402
403 let with_body = OcpiRequest::new(Method::PUT, url, ModuleId::Cdrs)
404 .with_body(&serde_json::json!({"a": 1}))
405 .unwrap()
406 .header_map(&CredentialsToken::new("t").unwrap(), &Quirks::default());
407 assert_eq!(with_body.get(http::header::CONTENT_TYPE).unwrap(), "application/json");
408 }
409
410 #[test]
411 fn retry_delays_grow_and_stay_under_the_cap() {
412 let policy = RetryPolicy::default();
413 let seed = RetryPolicy::seed_from("6d2b1b3a-0f8f-4e7e-9d3f-1a2b3c4d5e6f");
414 assert!(policy.delay_for(2, seed) > policy.delay_for(1, seed));
415 assert!(policy.delay_for(20, seed) <= policy.max_delay);
416 for attempt in 1..8 {
418 let base = policy.initial_delay.saturating_mul(1 << (attempt - 1)).min(policy.max_delay);
419 let delay = policy.delay_for(attempt, seed);
420 assert!(
421 delay >= base / 2 && delay <= base,
422 "attempt {attempt}: {delay:?} not in half of {base:?}"
423 );
424 }
425 }
426
427 #[test]
428 fn two_clients_retrying_the_same_endpoint_do_not_wait_the_same_time() {
429 let policy = RetryPolicy::default();
433 let delays: std::collections::HashSet<_> =
434 (0..64).map(|i| policy.delay_for(2, RetryPolicy::seed_from(&format!("request-{i}")))).collect();
435 assert!(delays.len() > 50, "only {} distinct delays across 64 requests", delays.len());
436 }
437
438 #[test]
439 fn a_policy_that_does_not_retry_waits_for_nothing() {
440 let policy = RetryPolicy::none();
441 assert_eq!(policy.delay_for(3, 12345), std::time::Duration::ZERO);
442 }
443
444 #[test]
445 fn transport_error_messages_do_not_leak_the_url() {
446 let message = "error sending request for url (https://e.com/cb?token=secret)";
447 assert_eq!(strip_url(message), "error sending request");
448 }
449}