Skip to main content

cratestack_axum/idempotency/
layer.rs

1//! Tower layer + companion `Service` constructor.
2
3use std::sync::Arc;
4use std::time::Duration;
5
6use axum::extract::Request;
7use http::header;
8use sha2::{Digest, Sha256};
9use tower::Layer;
10
11use super::service::IdempotencyService;
12use super::store::IdempotencyStore;
13
14/// Tower layer that wires an `IdempotencyStore` into the request pipeline.
15#[derive(Clone)]
16pub struct IdempotencyLayer {
17    pub(super) store: Arc<dyn IdempotencyStore>,
18    pub(super) ttl: Duration,
19    pub(super) principal_fingerprint: Arc<dyn Fn(&Request) -> String + Send + Sync>,
20}
21
22impl IdempotencyLayer {
23    /// Construct with a default principal fingerprint derived from the
24    /// `Authorization` header. Callers running mTLS or session-cookie auth
25    /// should swap this via [`with_principal_fingerprint`].
26    pub fn new(store: Arc<dyn IdempotencyStore>, ttl: Duration) -> Self {
27        Self {
28            store,
29            ttl,
30            principal_fingerprint: Arc::new(default_principal_fingerprint),
31        }
32    }
33
34    /// Override how the layer derives a principal-scoped namespace for the
35    /// idempotency key. Without this, two callers sharing a key (across
36    /// tenants) would collide.
37    pub fn with_principal_fingerprint(
38        mut self,
39        f: impl Fn(&Request) -> String + Send + Sync + 'static,
40    ) -> Self {
41        self.principal_fingerprint = Arc::new(f);
42        self
43    }
44}
45
46pub(super) fn default_principal_fingerprint(req: &Request) -> String {
47    req.headers()
48        .get(header::AUTHORIZATION)
49        .and_then(|v| v.to_str().ok())
50        .map(|s| {
51            let mut h = Sha256::new();
52            h.update(s.as_bytes());
53            format!("{:x}", h.finalize())
54        })
55        .unwrap_or_else(|| "anonymous".to_owned())
56}
57
58impl<S> Layer<S> for IdempotencyLayer {
59    type Service = IdempotencyService<S>;
60
61    fn layer(&self, inner: S) -> Self::Service {
62        IdempotencyService {
63            inner,
64            store: self.store.clone(),
65            ttl: self.ttl,
66            principal_fingerprint: self.principal_fingerprint.clone(),
67        }
68    }
69}