Expand description
Middleware for handling idempotent requests in axum applications.
This crate provides middleware that ensures idempotency of HTTP requests. When an identical request is made, a cached response is returned instead of re-executing the handler, preventing duplicate operations like accidental double payments.
§How it Works
The middleware operates in one of two modes:
-
Direct Key Mode (Recommended): By configuring
use_idempotency_key_header(), the middleware uses a client-provided header (e.g.,Idempotency-Key) value directly as the cache key. This is the most performant and observable method, as it avoids server-side hashing and uses an identifier known to both the client and server. -
Hashing Mode: If not using a direct key, a unique hash is generated from the request’s method, path, headers (configurable), and body. This hash is then used as the cache key.
If a key is found in the session store, the cached response is returned immediately. If not, the request is processed by the handler, and the response is cached before being sent to the client.
§Features
- Request deduplication using either a direct client-provided key or automatic request hashing.
- Configurable response caching duration.
- Fine-grained controls for hashing, including ignoring the request body or specific headers.
- Observability through a replay header (default:
idempotency-replayed) on cached responses. - Seamless integration with session-based storage via the
rutscrate.
§Example
use std::sync::Arc;
use axum::{Router, routing::post};
use ruts::{CookieOptions, SessionLayer};
use axum_idempotent::{IdempotentLayer, IdempotentOptions};
use tower_cookies::CookieManagerLayer;
use ruts::store::memory::MemoryStore;
#[tokio::main]
async fn main() {
// Your session store
let store = Arc::new(MemoryStore::new());
// Configure the idempotency layer to use the "Idempotency-Key" header
let idempotent_options = IdempotentOptions::default()
.use_idempotency_key_header(Some("Idempotency-Key"))
.expire_after(60 * 5); // Cache responses for 5 minutes
// Create the router
let app = Router::new()
.route("/payments", post(process_payment))
.layer(IdempotentLayer::<MemoryStore>::new(idempotent_options))
.layer(SessionLayer::new(store)
.with_cookie_options(CookieOptions::build().name("session")))
.layer(CookieManagerLayer::new());
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
async fn process_payment() -> &'static str {
"Payment processed"
}§Default Behavior
axum-idempotent is configured with safe defaults to prevent common issues.
§Ignored Status Codes
To avoid caching transient server errors or certain client errors, responses with the following HTTP status codes are not cached by default:
400 Bad Request401 Unauthorized403 Forbidden408 Request Timeout429 Too Many Requests500 Internal Server Error502 Bad Gateway503 Service Unavailable504 Gateway Timeout
§Ignored Headers
In hashing mode, common, request-specific headers
are ignored by default to ensure that requests from different clients are treated as
identical if the core parameters are the same. This does not apply when using
use_idempotency_key_header.
- user-agent,
- accept,
- accept-encoding,
- accept-language,
- cache-control,
- connection,
- cookie,
- host,
- pragma,
- referer,
- sec-fetch-dest,
- sec-fetch-mode,
- sec-fetch-site,
- sec-ch-ua,
- sec-ch-ua-mobile,
- sec-ch-ua-platform
Structs§
- Idempotent
Layer - Layer to apply
IdempotentServicemiddleware inaxum. - Idempotent
Options - Configuration options for the idempotency layer.
- Idempotent
Service - Service that handles idempotent request processing.