rust 64 lines · 8 steps

Idempotent payments with an Axum extractor

A custom extractor pulls the Idempotency-Key header and a shared cache replays prior responses so retries never double-charge.

Explained by highlit
1use axum::{
2 async_trait,
3 extract::{FromRequestParts, State},
4 http::{request::Parts, StatusCode},
5 response::{IntoResponse, Response},
6 Json,
7};
8use serde_json::json;
9use std::sync::Arc;
10use tokio::sync::Mutex;
11use std::collections::HashMap;
12 
13pub struct IdempotencyKey(pub String);
14 
15#[async_trait]
16impl<S> FromRequestParts<S> for IdempotencyKey
17where
18 S: Send + Sync,
19{
20 type Rejection = Response;
21 
22 async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
23 let raw = parts
24 .headers
25 .get("Idempotency-Key")
26 .and_then(|v| v.to_str().ok())
27 .filter(|v| !v.trim().is_empty())
28 .ok_or_else(|| {
29 (
30 StatusCode::BAD_REQUEST,
31 Json(json!({ "error": "missing or empty Idempotency-Key header" })),
32 )
33 .into_response()
34 })?;
35 
36 Ok(IdempotencyKey(raw.to_owned()))
37 }
38}
39 
40#[derive(Clone, Default)]
41pub struct IdempotencyCache {
42 inner: Arc<Mutex<HashMap<String, (StatusCode, serde_json::Value)>>>,
43}
44 
45pub async fn create_payment(
46 State(cache): State<IdempotencyCache>,
47 IdempotencyKey(key): IdempotencyKey,
48 Json(payload): Json<serde_json::Value>,
49) -> Response {
50 let mut store = cache.inner.lock().await;
51 
52 if let Some((status, body)) = store.get(&key) {
53 return (*status, Json(body.clone())).into_response();
54 }
55 
56 let charged = json!({
57 "id": uuid::Uuid::new_v4(),
58 "status": "succeeded",
59 "amount": payload["amount"].clone(),
60 });
61 
62 store.insert(key, (StatusCode::CREATED, charged.clone()));
63 (StatusCode::CREATED, Json(charged)).into_response()
64}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1A custom FromRequestParts extractor lets you validate and reject a request before your handler ever runs.
  2. 2Keying responses by a client-supplied token turns unsafe retries into safe, replayable operations.
  3. 3Wrapping shared mutable state in Arc<Mutex<..>> lets many concurrent handlers share one cache safely.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Idempotent payments with an Axum extractor — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code