barnacle-rs

Crates.iobarnacle-rs
lib.rsbarnacle-rs
version0.3.0
created_at2025-06-30 18:13:10.968262+00
updated_at2025-07-24 08:02:04.103624+00
descriptionAdvanced rate limiting middleware for Axum with Redis backend, API key validation, and custom key extraction
homepagehttps://github.com/zyphelabs/barnacle-rs
repositoryhttps://github.com/zyphelabs/barnacle-rs
max_upload_size
id1732156
size3,401,140
Adriano Dalpane (digitalillusion)

documentation

README

Barnacle Logo

Barnacle 🦀

Crates.io Documentation License Rust Version

Rate limiting and API key validation middleware for Axum with Redis backend.

Repository | Documentation | Crates.io

Features

  • Rate Limiting: IP-based or custom key-based rate limiting
  • API Key Validation: Validate x-api-key header with per-key limits
  • Redis Backend: Distributed rate limiting with Redis
  • Axum Middleware: Drop-in middleware for Axum applications
  • Reset on Success: Optional rate limit reset on successful operations
  • Extensible Design: Custom key stores and rate limiting strategies

Examples

Quick Start

[dependencies]
barnacle-rs = "0.3"
axum = "0.8"
tokio = { version = "1", features = ["full"] }

Basic Rate Limiting

use barnacle_rs::{RedisBarnacleStore, BarnacleConfig};
use axum::{Router, routing::get};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let store = RedisBarnacleStore::from_url("redis://127.0.0.1:6379").await?;
    let config = BarnacleConfig {
        max_requests: 10,
        window: std::time::Duration::from_secs(60),
        reset_on_success: barnacle_rs::ResetOnSuccess::Not,
    };
    let layer = barnacle_rs::BarnacleLayer::builder()
        .with_store(store)
        .with_config(config)
        .build();
    let app = Router::new()
        .route("/api/data", get(handler))
        .layer(layer);
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
    axum::serve(listener, app).await?;
    Ok(())
}

async fn handler() -> &'static str {
    "Hello, World!"
}

API Key Validation (Stateless)

use barnacle_rs::{BarnacleLayer, BarnacleConfig, RedisBarnacleStore, BarnacleError};
use axum::{Router, routing::get};
use std::sync::Arc;
use axum::http::request::Parts;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let store = RedisBarnacleStore::from_url("redis://127.0.0.1:6379").await?;
    let config = BarnacleConfig::default();
    let api_key_validator = |api_key: String, _api_key_config: ApiKeyConfig, _parts: Arc<Parts>, _state: ()| async move {
        if api_key.is_empty() {
            Err(BarnacleError::ApiKeyMissing)
        } else if api_key != "test-key" {
            Err(BarnacleError::invalid_api_key(api_key))
        } else {
            Ok(())
        }
    };
    let layer: BarnacleLayer<(), RedisBarnacleStore, (), BarnacleError, _> = BarnacleLayer::builder()
        .with_store(store)
        .with_config(config)
        .with_api_key_validator(api_key_validator)
        .build()
        .unwrap();
    let app = Router::new()
        .route("/api/protected", get(handler))
        .layer(layer);
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
    axum::serve(listener, app).await?;
    Ok(())
}

async fn handler() -> &'static str {
    "Protected endpoint"
}

API Key Validation (With state)

use barnacle_rs::{BarnacleLayer, BarnacleConfig, RedisBarnacleStore, BarnacleError};
use axum::{Router, routing::get};
use std::sync::Arc;
use axum::http::request::Parts;

#[derive(Clone)]
struct MyState {
    allowed_keys: Vec<String>,
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let store = RedisBarnacleStore::from_url("redis://127.0.0.1:6379").await?;
    let config = BarnacleConfig::default();
    let state = MyState { allowed_keys: vec!["my-secret-key".to_string()] };
    let api_key_validator = |api_key: String, _api_key_config: BarnacleConfig, _parts: Arc<Parts>, state: MyState| async move {
        let allowed = state.allowed_keys.contains(&api_key);
        if allowed {
            Ok(())
        } else {
            Err(BarnacleError::invalid_api_key(api_key))
        }
    };
    let layer: BarnacleLayer<(), RedisBarnacleStore, MyState, BarnacleError, _> = BarnacleLayer::builder()
        .with_store(store)
        .with_config(config)
        .with_state(state)
        .with_api_key_validator(api_key_validator)
        .build()
        .unwrap();
    let app = Router::new()
        .route("/api/protected", get(handler))
        .layer(layer);
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
    axum::serve(listener, app).await?;
    Ok(())
}

async fn handler() -> &'static str {
    "Protected endpoint with state"
}

Custom Key Extraction (e.g., Email)

use barnacle_rs::{KeyExtractable, BarnacleKey};
use axum::http::request::Parts;

#[derive(serde::Deserialize)]
struct LoginRequest {
    email: String,
    password: String,
}
impl KeyExtractable for LoginRequest {
    fn extract_key(&self) -> BarnacleKey {
        BarnacleKey::Email(self.email.clone())
    }
}
let layer = barnacle_rs::BarnacleLayer::builder()
    .with_store(store)
    .with_config(config)
    .build();

Rate Limiting Strategies

IP-based (default)

let layer = barnacle_rs::BarnacleLayer::builder()
    .with_store(store)
    .with_config(config)
    .build();

API Key-based

let layer = barnacle_rs::BarnacleLayer::builder()
    .with_store(api_key_store)
    .with_config(config)
    .build();

Custom Key (e.g., email)

use barnacle_rs::{KeyExtractable, BarnacleKey};

#[derive(serde::Deserialize)]
struct LoginRequest {
    email: String,
    password: String,
}

impl KeyExtractable for LoginRequest {
    fn extract_key(&self) -> BarnacleKey {
        BarnacleKey::Email(self.email.clone())
    }

}

let layer = barnacle_rs::BarnacleLayer::builder()
    .with_store(store)
    .with_config(config)
    .build();

Example: No Validator (API key validation disabled)

use barnacle_rs::{BarnacleLayer, RedisBarnacleStore, BarnacleError};

let middleware: BarnacleLayer<(), RedisBarnacleStore, (), BarnacleError, ()> = BarnacleLayer::builder()
    .with_store(store)
    .with_config(config)
    .build()
    .unwrap();

Example: With Validator (API key validation enabled)

use barnacle_rs::{BarnacleLayer, RedisBarnacleStore, BarnacleError};
use std::sync::Arc;
use axum::http::request::Parts;

let api_key_validator = |api_key: String, api_key_config: ApiKeyConfig, parts: Arc<Parts>, state: ()| async move {
    if api_key == "test-key" {
        Ok(())
    } else {
        Err(BarnacleError::invalid_api_key(api_key))
    }
};

let middleware: BarnacleLayer<(), RedisBarnacleStore, (), BarnacleError, _> = BarnacleLayer::builder()
    .with_store(store)
    .with_config(config)
    .with_api_key_validator(api_key_validator)
    .with_state(())
    .build()
    .unwrap();

Note:

  • The validator closure must take owned arguments: (String, ApiKeyConfig, Arc<Parts>, State).
  • If you do not provide a validator, use () for the last type parameter.
  • If you provide a validator, use _ for the last type parameter to let Rust infer the closure type.

Running Examples

# Run examples
cargo run --example basic
cargo run --example api_key_redis_test
cargo run --example custom_validator_example
cargo run --example error_integration
cargo run --example api_key_test

Error Integration & Custom Validator

For error handling and custom validator implementation, see:

  • examples/error_integration.rs
  • examples/custom_validator_example.rs

Configuration

let config = BarnacleConfig {
    max_requests: 100,                              // Requests per window
    window: Duration::from_secs(3600),              // Time window
    reset_on_success: ResetOnSuccess::Yes(          // Reset on success
        Some(vec![200, 201])                        // Status codes to reset on
    ),
};

Automatic Route-Based Rate Limiting

Barnacle automatically includes route information (path and method) in Redis keys, providing per-endpoint rate limiting without any additional configuration:

Redis Key Format:

barnacle:email:user@example.com:POST:/auth/login
barnacle:email:user@example.com:POST:/auth/start-reset
barnacle:api_keys:your-key:GET:/api/data
barnacle:ip:192.168.1.1:POST:/api/submit

This means:

  • ✅ Same email can have different rate limits per endpoint
  • ✅ No need to modify KeyExtractable implementations
  • ✅ Automatic separation of rate limits by route
  • ✅ Backward compatible with existing code

Redis Setup

Store API keys in Redis:

# Valid API key
redis-cli SET "barnacle:api_keys:your-key" 1

# Per-key rate limit config
redis-cli SET "barnacle:api_keys:config:your-key" '{"max_requests":100,"window":3600,"reset_on_success":"Not"}'

License

MIT

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

Commit count: 0

cargo fmt