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::{BuildHasher, 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(100_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 required: bool,
184 scope: Arc<ScopeFn>,
185 hasher: std::collections::hash_map::RandomState,
186}
187
188impl Default for Idempotency {
189 fn default() -> Self {
190 Self::new()
191 }
192}
193
194impl Idempotency {
195 pub fn new() -> Self {
197 Self::with_store(MemoryStore::default())
198 }
199
200 pub fn with_store(store: impl IdempotencyStore) -> Self {
201 let hasher = std::collections::hash_map::RandomState::new();
202 let scope_hasher = hasher.clone();
203 Self {
204 store: Arc::new(store),
205 header: "idempotency-key".into(),
206 methods: vec![Method::POST, Method::PATCH],
207 ttl: Duration::from_secs(24 * 3600),
208 lock_ttl: Duration::from_secs(60),
209 required: false,
210 scope: Arc::new(move |req| {
211 let mut h = scope_hasher.build_hasher();
212 for name in ["authorization", "cookie"] {
213 req.headers.get(name).map(|v| v.as_bytes()).hash(&mut h);
214 }
215 format!("{:016x}", h.finish())
216 }),
217 hasher,
218 }
219 }
220
221 pub fn methods<I: IntoIterator<Item = Method>>(mut self, methods: I) -> Self {
224 self.methods = methods.into_iter().collect();
225 self
226 }
227
228 pub fn ttl(mut self, ttl: Duration) -> Self {
230 self.ttl = ttl;
231 self
232 }
233
234 pub fn lock_ttl(mut self, lock_ttl: Duration) -> Self {
236 self.lock_ttl = lock_ttl;
237 self
238 }
239
240 pub fn required(mut self) -> Self {
243 self.required = true;
244 self
245 }
246
247 pub fn header(mut self, name: &str) -> Self {
249 self.header = name.to_ascii_lowercase();
250 self
251 }
252
253 pub fn scope_by<F>(mut self, f: F) -> Self
256 where
257 F: Fn(&Request) -> String + Send + Sync + 'static,
258 {
259 self.scope = Arc::new(f);
260 self
261 }
262
263 fn fingerprint(&self, req: &Request) -> u64 {
264 let mut h = self.hasher.build_hasher();
265 req.method.as_str().hash(&mut h);
266 req.uri.path().hash(&mut h);
267 req.uri.query().hash(&mut h);
268 req.body.hash(&mut h);
269 h.finish()
270 }
271}
272
273struct Claim {
275 store: Arc<dyn IdempotencyStore>,
276 key: String,
277 armed: bool,
278}
279
280impl Drop for Claim {
281 fn drop(&mut self) {
282 if self.armed {
283 let (store, key) = (self.store.clone(), std::mem::take(&mut self.key));
284 if let Ok(handle) = tokio::runtime::Handle::try_current() {
285 handle.spawn(async move { store.abandon(&key).await });
286 }
287 }
288 }
289}
290
291fn valid_key(key: &str) -> bool {
292 !key.is_empty() && key.len() <= 255 && key.bytes().all(|b| (0x21..=0x7e).contains(&b))
293}
294
295#[async_trait]
296impl Middleware for Idempotency {
297 async fn handle(&self, req: Request, next: Next) -> Result<Response> {
298 if !self.methods.contains(&req.method) {
299 return next.run(req).await;
300 }
301 let key = match req.headers.get(self.header.as_str()) {
302 None if self.required => {
303 return Ok(Response::from_error(Error::bad_request(format!(
304 "{} header is required",
305 self.header
306 ))))
307 }
308 None => return next.run(req).await,
309 Some(v) => match v.to_str().ok().filter(|k| valid_key(k)) {
310 Some(k) => k.to_string(),
311 None => {
312 return Ok(Response::from_error(Error::bad_request(format!(
313 "{} must be 1-255 visible ASCII characters",
314 self.header
315 ))))
316 }
317 },
318 };
319
320 let store_key = format!("{}|{}", (self.scope)(&req), key);
321 let fingerprint = self.fingerprint(&req);
322 match self
323 .store
324 .begin(&store_key, fingerprint, self.lock_ttl)
325 .await
326 {
327 Begin::Started => {}
328 Begin::InFlight => {
329 return Ok(Response::from_error(Error::custom(
330 409,
331 "A request with this idempotency key is still being processed",
332 ))
333 .with_header("retry-after", "1"))
334 }
335 Begin::Mismatch => {
336 return Ok(Response::from_error(Error::custom(
337 422,
338 "Idempotency key was already used with a different request",
339 )))
340 }
341 Begin::Unavailable => {
342 return Ok(Response::from_error(Error::custom(
343 503,
344 "Idempotency store is full, try again shortly",
345 ))
346 .with_header("retry-after", "1"))
347 }
348 Begin::Replay(stored) => return Ok(replay(stored)),
349 }
350
351 let mut claim = Claim {
352 store: self.store.clone(),
353 key: store_key.clone(),
354 armed: true,
355 };
356 let res = next.run(req).await.unwrap_or_else(Response::from_error);
357 if res.status.is_server_error() || res.is_streaming() {
358 return Ok(res); }
360 claim.armed = false;
361 let stored = StoredResponse {
362 status: res.status.as_u16(),
363 headers: res
364 .headers
365 .iter()
366 .filter(|(n, _)| *n != http::header::SET_COOKIE)
367 .map(|(n, v)| (n.as_str().to_string(), v.as_bytes().to_vec()))
368 .collect(),
369 body: res.body.clone(),
370 };
371 self.store.complete(&store_key, stored, self.ttl).await;
372 Ok(res)
373 }
374}
375
376fn replay(stored: StoredResponse) -> Response {
377 let mut res =
378 Response::new(http::StatusCode::from_u16(stored.status).unwrap_or(http::StatusCode::OK))
379 .with_body(stored.body);
380 for (name, value) in stored.headers {
381 if let (Ok(n), Ok(v)) = (
382 http::HeaderName::from_bytes(name.as_bytes()),
383 http::HeaderValue::from_bytes(&value),
384 ) {
385 res.headers.append(n, v);
386 }
387 }
388 res.with_header("idempotency-replayed", "true")
389}
390
391#[cfg(test)]
392mod tests {
393 use super::*;
394
395 fn done() -> StoredResponse {
396 StoredResponse {
397 status: 201,
398 headers: vec![],
399 body: Bytes::from_static(b"x"),
400 }
401 }
402
403 #[tokio::test]
404 async fn lifecycle_in_memory() {
405 let s = MemoryStore::default();
406 let ttl = Duration::from_secs(10);
407 assert!(matches!(s.begin("k", 1, ttl).await, Begin::Started));
408 assert!(matches!(s.begin("k", 1, ttl).await, Begin::InFlight));
409 assert!(matches!(s.begin("k", 2, ttl).await, Begin::Mismatch));
410 s.complete("k", done(), ttl).await;
411 assert!(matches!(s.begin("k", 1, ttl).await, Begin::Replay(r) if r.status == 201));
412 assert!(matches!(s.begin("k", 2, ttl).await, Begin::Mismatch));
413 }
414
415 #[tokio::test]
416 async fn abandon_frees_running_but_not_finished_keys() {
417 let s = MemoryStore::default();
418 let ttl = Duration::from_secs(10);
419 s.begin("a", 1, ttl).await;
420 s.abandon("a").await;
421 assert!(matches!(s.begin("a", 1, ttl).await, Begin::Started));
422 s.complete("a", done(), ttl).await;
423 s.abandon("a").await;
424 assert!(matches!(s.begin("a", 1, ttl).await, Begin::Replay(_)));
425 }
426
427 #[test]
428 fn expired_claims_and_results_lapse() {
429 let s = MemoryStore::default();
430 let t = Instant::now();
431 assert!(matches!(
432 s.begin_at("k", 1, Duration::from_secs(5), t),
433 Begin::Started
434 ));
435 assert!(matches!(
436 s.begin_at("k", 1, Duration::from_secs(5), t + Duration::from_secs(6)),
437 Begin::Started
438 ));
439 }
440
441 #[test]
442 fn full_store_refuses_new_keys_until_something_expires() {
443 let s = MemoryStore::new(1);
444 let t = Instant::now();
445 let ttl = Duration::from_secs(5);
446 assert!(matches!(s.begin_at("a", 1, ttl, t), Begin::Started));
447 assert!(matches!(s.begin_at("b", 1, ttl, t), Begin::Unavailable));
448 assert!(matches!(
449 s.begin_at("b", 1, ttl, t + Duration::from_secs(6)),
450 Begin::Started
451 ));
452 }
453
454 #[test]
455 fn key_validation() {
456 assert!(valid_key("3f9c-aa"));
457 assert!(!valid_key(""));
458 assert!(!valid_key("has space"));
459 assert!(!valid_key(&"x".repeat(256)));
460 assert!(!valid_key("ümlaut"));
461 }
462}