1use crate::error::{Error, Result};
42use crate::middleware::{Middleware, Next};
43use crate::request::Request;
44use crate::response::Response;
45use async_trait::async_trait;
46use bytes::Bytes;
47use http::Method;
48use std::collections::HashMap;
49use std::hash::{DefaultHasher, Hash, Hasher};
50use std::sync::{Arc, Mutex};
51use std::time::{Duration, Instant};
52
53#[derive(Clone, Debug)]
55pub struct StoredResponse {
56 pub status: u16,
57 pub headers: Vec<(String, Vec<u8>)>,
58 pub body: Bytes,
59}
60
61#[derive(Debug)]
63pub enum Begin {
64 Started,
66 InFlight,
68 Replay(StoredResponse),
70 Mismatch,
72 Unavailable,
74}
75
76#[async_trait]
78pub trait IdempotencyStore: Send + Sync + 'static {
79 async fn begin(&self, key: &str, fingerprint: u64, lock_ttl: Duration) -> Begin;
82 async fn complete(&self, key: &str, response: StoredResponse, ttl: Duration);
84 async fn abandon(&self, key: &str);
86}
87
88enum State {
89 Running,
90 Done(StoredResponse),
91}
92
93struct Entry {
94 fingerprint: u64,
95 state: State,
96 expires: Instant,
97}
98
99pub struct MemoryStore {
101 entries: Mutex<HashMap<String, Entry>>,
102 max_entries: usize,
103}
104
105impl Default for MemoryStore {
106 fn default() -> Self {
107 Self::new(10_000)
108 }
109}
110
111impl MemoryStore {
112 pub fn new(max_entries: usize) -> Self {
114 Self {
115 entries: Mutex::new(HashMap::new()),
116 max_entries,
117 }
118 }
119
120 fn begin_at(&self, key: &str, fingerprint: u64, lock_ttl: Duration, now: Instant) -> Begin {
121 let mut map = self.entries.lock().unwrap_or_else(|e| e.into_inner());
122 if let Some(entry) = map.get(key) {
123 if entry.expires > now {
124 return if entry.fingerprint != fingerprint {
125 Begin::Mismatch
126 } else {
127 match &entry.state {
128 State::Running => Begin::InFlight,
129 State::Done(res) => Begin::Replay(res.clone()),
130 }
131 };
132 }
133 }
134 if map.len() >= self.max_entries && !map.contains_key(key) {
135 map.retain(|_, e| e.expires > now);
136 if map.len() >= self.max_entries {
137 return Begin::Unavailable;
138 }
139 }
140 map.insert(
141 key.to_string(),
142 Entry {
143 fingerprint,
144 state: State::Running,
145 expires: now + lock_ttl,
146 },
147 );
148 Begin::Started
149 }
150}
151
152#[async_trait]
153impl IdempotencyStore for MemoryStore {
154 async fn begin(&self, key: &str, fingerprint: u64, lock_ttl: Duration) -> Begin {
155 self.begin_at(key, fingerprint, lock_ttl, Instant::now())
156 }
157
158 async fn complete(&self, key: &str, response: StoredResponse, ttl: Duration) {
159 let mut map = self.entries.lock().unwrap_or_else(|e| e.into_inner());
160 if let Some(entry) = map.get_mut(key) {
161 entry.state = State::Done(response);
162 entry.expires = Instant::now() + ttl;
163 }
164 }
165
166 async fn abandon(&self, key: &str) {
167 let mut map = self.entries.lock().unwrap_or_else(|e| e.into_inner());
168 if matches!(map.get(key).map(|e| &e.state), Some(State::Running)) {
169 map.remove(key);
170 }
171 }
172}
173
174type ScopeFn = dyn Fn(&Request) -> String + Send + Sync;
175
176pub struct Idempotency {
178 store: Arc<dyn IdempotencyStore>,
179 header: String,
180 methods: Vec<Method>,
181 ttl: Duration,
182 lock_ttl: Duration,
183 max_response: usize,
184 required: bool,
185 scope: Option<Arc<ScopeFn>>,
187 seed: Vec<u8>,
189}
190
191impl Default for Idempotency {
192 fn default() -> Self {
193 Self::new()
194 }
195}
196
197impl Idempotency {
198 pub fn new() -> Self {
200 Self::with_store(MemoryStore::default())
201 }
202
203 pub fn with_store(store: impl IdempotencyStore) -> Self {
204 let seed = crate::security::random_bytes(16).unwrap_or_else(|_| {
206 let state = std::collections::hash_map::RandomState::new();
207 let mut h = std::hash::BuildHasher::build_hasher(&state);
208 h.write_u8(0);
209 h.finish().to_le_bytes().to_vec()
210 });
211 Self {
212 store: Arc::new(store),
213 header: "idempotency-key".into(),
214 methods: vec![Method::POST, Method::PATCH],
215 ttl: Duration::from_secs(24 * 3600),
216 lock_ttl: Duration::from_secs(60),
217 max_response: 256 * 1024,
218 required: false,
219 scope: None,
220 seed,
221 }
222 }
223
224 pub fn shared_seed(mut self, seed: impl AsRef<[u8]>) -> Self {
231 self.seed = seed.as_ref().to_vec();
232 self
233 }
234
235 fn hasher(&self) -> DefaultHasher {
236 let mut h = DefaultHasher::new();
237 h.write(&self.seed);
238 h
239 }
240
241 fn scope_of(&self, req: &Request) -> String {
242 match &self.scope {
243 Some(f) => f(req),
244 None => {
245 let mut h = self.hasher();
246 for name in ["authorization", "cookie"] {
247 req.headers.get(name).map(|v| v.as_bytes()).hash(&mut h);
248 }
249 format!("{:016x}", h.finish())
250 }
251 }
252 }
253
254 pub fn methods<I: IntoIterator<Item = Method>>(mut self, methods: I) -> Self {
257 self.methods = methods.into_iter().collect();
258 self
259 }
260
261 pub fn ttl(mut self, ttl: Duration) -> Self {
263 self.ttl = ttl;
264 self
265 }
266
267 pub fn lock_ttl(mut self, lock_ttl: Duration) -> Self {
269 self.lock_ttl = lock_ttl;
270 self
271 }
272
273 pub fn max_response_bytes(mut self, max: usize) -> Self {
276 self.max_response = max;
277 self
278 }
279
280 pub fn required(mut self) -> Self {
283 self.required = true;
284 self
285 }
286
287 pub fn header(mut self, name: &str) -> Self {
289 self.header = name.to_ascii_lowercase();
290 self
291 }
292
293 pub fn scope_by<F>(mut self, f: F) -> Self
296 where
297 F: Fn(&Request) -> String + Send + Sync + 'static,
298 {
299 self.scope = Some(Arc::new(f));
300 self
301 }
302
303 fn fingerprint(&self, req: &Request) -> u64 {
304 let mut h = self.hasher();
305 req.method.as_str().hash(&mut h);
306 req.uri.path().hash(&mut h);
307 req.uri.query().hash(&mut h);
308 req.body.hash(&mut h);
309 h.finish()
310 }
311}
312
313struct Claim {
315 store: Arc<dyn IdempotencyStore>,
316 key: String,
317 armed: bool,
318}
319
320impl Drop for Claim {
321 fn drop(&mut self) {
322 if self.armed {
323 let (store, key) = (self.store.clone(), std::mem::take(&mut self.key));
324 if let Ok(handle) = tokio::runtime::Handle::try_current() {
325 handle.spawn(async move { store.abandon(&key).await });
326 }
327 }
328 }
329}
330
331fn valid_key(key: &str) -> bool {
332 !key.is_empty() && key.len() <= 255 && key.bytes().all(|b| (0x21..=0x7e).contains(&b))
333}
334
335#[async_trait]
336impl Middleware for Idempotency {
337 async fn handle(&self, req: Request, next: Next) -> Result<Response> {
338 if !self.methods.contains(&req.method) {
339 return next.run(req).await;
340 }
341 let key = match req.headers.get(self.header.as_str()) {
342 None if self.required => {
343 return Ok(Response::from_error(Error::bad_request(format!(
344 "{} header is required",
345 self.header
346 ))))
347 }
348 None => return next.run(req).await,
349 Some(v) => match v.to_str().ok().filter(|k| valid_key(k)) {
350 Some(k) => k.to_string(),
351 None => {
352 return Ok(Response::from_error(Error::bad_request(format!(
353 "{} must be 1-255 visible ASCII characters",
354 self.header
355 ))))
356 }
357 },
358 };
359
360 let store_key = format!("{}|{}", self.scope_of(&req), key);
361 let fingerprint = self.fingerprint(&req);
362 match self
363 .store
364 .begin(&store_key, fingerprint, self.lock_ttl)
365 .await
366 {
367 Begin::Started => {}
368 Begin::InFlight => {
369 return Ok(Response::from_error(Error::custom(
370 409,
371 "A request with this idempotency key is still being processed",
372 ))
373 .with_header("retry-after", "1"))
374 }
375 Begin::Mismatch => {
376 return Ok(Response::from_error(Error::custom(
377 422,
378 "Idempotency key was already used with a different request",
379 )))
380 }
381 Begin::Unavailable => {
382 return Ok(Response::from_error(Error::custom(
383 503,
384 "Idempotency store is full, try again shortly",
385 ))
386 .with_header("retry-after", "1"))
387 }
388 Begin::Replay(stored) => return Ok(replay(stored)),
389 }
390
391 let mut claim = Claim {
392 store: self.store.clone(),
393 key: store_key.clone(),
394 armed: true,
395 };
396 let res = next.run(req).await.unwrap_or_else(Response::from_error);
397 if res.status.is_server_error() || res.is_streaming() || res.body.len() > self.max_response
398 {
399 return Ok(res); }
401 claim.armed = false;
402 let stored = StoredResponse {
403 status: res.status.as_u16(),
404 headers: res
405 .headers
406 .iter()
407 .filter(|(n, _)| *n != http::header::SET_COOKIE)
408 .map(|(n, v)| (n.as_str().to_string(), v.as_bytes().to_vec()))
409 .collect(),
410 body: res.body.clone(),
411 };
412 self.store.complete(&store_key, stored, self.ttl).await;
413 Ok(res)
414 }
415}
416
417fn replay(stored: StoredResponse) -> Response {
418 let mut res =
419 Response::new(http::StatusCode::from_u16(stored.status).unwrap_or(http::StatusCode::OK))
420 .with_body(stored.body);
421 for (name, value) in stored.headers {
422 if let (Ok(n), Ok(v)) = (
423 http::HeaderName::from_bytes(name.as_bytes()),
424 http::HeaderValue::from_bytes(&value),
425 ) {
426 res.headers.append(n, v);
427 }
428 }
429 res.with_header("idempotency-replayed", "true")
430}
431
432#[cfg(test)]
433mod tests {
434 use super::*;
435
436 fn done() -> StoredResponse {
437 StoredResponse {
438 status: 201,
439 headers: vec![],
440 body: Bytes::from_static(b"x"),
441 }
442 }
443
444 #[tokio::test]
445 async fn lifecycle_in_memory() {
446 let s = MemoryStore::default();
447 let ttl = Duration::from_secs(10);
448 assert!(matches!(s.begin("k", 1, ttl).await, Begin::Started));
449 assert!(matches!(s.begin("k", 1, ttl).await, Begin::InFlight));
450 assert!(matches!(s.begin("k", 2, ttl).await, Begin::Mismatch));
451 s.complete("k", done(), ttl).await;
452 assert!(matches!(s.begin("k", 1, ttl).await, Begin::Replay(r) if r.status == 201));
453 assert!(matches!(s.begin("k", 2, ttl).await, Begin::Mismatch));
454 }
455
456 #[tokio::test]
457 async fn abandon_frees_running_but_not_finished_keys() {
458 let s = MemoryStore::default();
459 let ttl = Duration::from_secs(10);
460 s.begin("a", 1, ttl).await;
461 s.abandon("a").await;
462 assert!(matches!(s.begin("a", 1, ttl).await, Begin::Started));
463 s.complete("a", done(), ttl).await;
464 s.abandon("a").await;
465 assert!(matches!(s.begin("a", 1, ttl).await, Begin::Replay(_)));
466 }
467
468 #[test]
469 fn expired_claims_and_results_lapse() {
470 let s = MemoryStore::default();
471 let t = Instant::now();
472 assert!(matches!(
473 s.begin_at("k", 1, Duration::from_secs(5), t),
474 Begin::Started
475 ));
476 assert!(matches!(
477 s.begin_at("k", 1, Duration::from_secs(5), t + Duration::from_secs(6)),
478 Begin::Started
479 ));
480 }
481
482 #[test]
483 fn full_store_refuses_new_keys_until_something_expires() {
484 let s = MemoryStore::new(1);
485 let t = Instant::now();
486 let ttl = Duration::from_secs(5);
487 assert!(matches!(s.begin_at("a", 1, ttl, t), Begin::Started));
488 assert!(matches!(s.begin_at("b", 1, ttl, t), Begin::Unavailable));
489 assert!(matches!(
490 s.begin_at("b", 1, ttl, t + Duration::from_secs(6)),
491 Begin::Started
492 ));
493 }
494
495 #[test]
496 fn key_validation() {
497 assert!(valid_key("3f9c-aa"));
498 assert!(!valid_key(""));
499 assert!(!valid_key("has space"));
500 assert!(!valid_key(&"x".repeat(256)));
501 assert!(!valid_key("ümlaut"));
502 }
503}