#axum #idempotency #web-apps #session-store #http-request

axum-idempotent

A middleware for handling idempotent requests in axum web applications

2 releases

new 0.1.1 Feb 7, 2025
0.1.0 Feb 7, 2025

#832 in Network programming

Download history 203/week @ 2025-02-03

203 downloads per month

MIT license

28KB
395 lines

axum-idempotent

Documentation Crates.io License: MIT Rust

Middleware for handling idempotent requests in axum web applications.

This crate provides middleware that ensures idempotency of HTTP requests by caching responses in a session store. When an identical request is made within the configured time window, the cached response is returned instead of executing the request again.

Features

  • Request deduplication based on method, path, headers, and body
  • Configurable response caching duration
  • Header filtering options to exclude specific headers from idempotency checks
  • Integration with session-based storage (via ruts)

Dependencies

This middleware requires the SessionLayer from the ruts crate. The layers must be added in the correct order:

  1. SessionLayer
  2. IdempotentLayer

Usage

Add this to your Cargo.toml:

[dependencies]
axum-idempotent = "0.1.1"

Example

use std::sync::Arc;
use axum::{Router, routing::post};
use fred::clients::Client;
use fred::interfaces::ClientLike;
use ruts::{CookieOptions, SessionLayer};
use axum_idempotent::{IdempotentLayer, IdempotentOptions};
use ruts::store::redis::RedisStore;
use tower_cookies::CookieManagerLayer;

#[tokio::main]
async fn main() {
    let client = Client::default();
    client.init().await.unwrap();
    let store = Arc::new(RedisStore::new(Arc::new(client)));

    // Configure the idempotency layer
    let idempotent_options = IdempotentOptions::default()
        .expire_after(60)  // Cache responses for 60 seconds
        .ignore_header("x-request-id".parse().unwrap());

    // Create the router with idempotency middleware
    let app = Router::new()
        .route("/payments", post(process_payment))
        .layer(IdempotentLayer::<RedisStore<Client>>::new(idempotent_options))
        .layer(SessionLayer::new(store)
            .with_cookie_options(CookieOptions::build()
                .name("session")
                .max_age(3600)
                .path("/")))
        .layer(CookieManagerLayer::new());
}

async fn process_payment() -> &'static str {
    "Payment processed"
}

How it Works

  1. When a request is received, a hash is generated from the request method, path, headers (configurable), and body.
  2. If an identical request (same hash) is found in the session store and hasn't expired:
    • The cached response is returned
    • The original handler is not called
  3. If no cached response is found:
    • The request is processed normally
    • The response is cached in the session store
    • The response is returned to the client

This ensures that retrying the same request (e.g., due to network issues or client retries) won't result in the operation being performed multiple times.

License

This project is licensed under the MIT License.

Dependencies

~8–15MB
~175K SLoC