Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.esperr.com/llms.txt

Use this file to discover all available pages before exploring further.

Start With Integrations For Faster SetupIf you want a recipe-backed path for a specific platform, start with the Integrations guides. The SDK is best when you want direct control inside your own service or gateway layer.

Installation

Add to your Cargo.toml:
[dependencies]
esper-rs = "0.1.0"

# For async support
tokio = { version = "1", features = ["full"] }

Basic Usage

Synchronous Client

use esper_rs::{BeaconEvent, EsperClient, EsperConfig, MitigationRequest};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Initialize client
    let config = EsperConfig::new("your-api-key");
    let client = EsperClient::new(config)?;

    // Send the first request's traffic to Esper beacon for analysis.
    let event = BeaconEvent::new("http_request")
        .with_ip("192.168.1.1")
        .with_user_agent("Mozilla/5.0...")
        .with_path("/login")
        .with_method("POST");

    client.send_beacon_event(&event)?;

    // Check a later request against active mitigation state.
    let request = MitigationRequest::new("192.168.1.1")
        .with_user_agent("Mozilla/5.0...")
        .with_path("/checkout")
        .with_method("POST")
        .with_metadata("request_id", serde_json::json!("req-2"))
        .with_metadata("session_id", serde_json::json!("session-123"));

    let response = client.check_mitigation(&request)?;

    match response.action {
        esper_rs::MitigationAction::Allow => {
            println!("Request allowed");
        }
        esper_rs::MitigationAction::Block => {
            println!("Request blocked: {:?}", response.reason);
        }
        esper_rs::MitigationAction::Challenge => {
            println!("Challenge required");
        }
    }

    Ok(())
}

Asynchronous Client

use esper_rs::{AsyncEsperClient, EsperConfig, MitigationRequest};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let config = EsperConfig::new("your-api-key");
    let client = AsyncEsperClient::new(config)?;

    let request = MitigationRequest::new("192.168.1.1")
        .with_user_agent("Mozilla/5.0...")
        .with_path("/api/endpoint");

    let response = client.check_mitigation(&request).await?;

    println!("Action: {:?}", response.action);
    Ok(())
}

Configuration

use esper_rs::EsperConfig;

let config = EsperConfig::new("your-api-key")
    .with_api_url("https://api.esperr.com")      // Optional
    .with_timeout(30)                             // Optional (seconds)
    .with_max_retries(3);                         // Optional

API Methods

check_mitigation(&request)

Check if a request should be mitigated.
use esper_rs::{MitigationRequest, MitigationAction};

let request = MitigationRequest::new("192.168.1.1")
    .with_user_agent("Mozilla/5.0...")
    .with_path("/api/endpoint")
    .with_method("POST")
    .with_header("X-Custom", "value")
    .with_metadata("user_id", serde_json::json!("123"));

let response = client.check_mitigation(&request)?;

match response.action {
    MitigationAction::Allow => {
        println!("Allowed with score: {:?}", response.score);
    }
    MitigationAction::Block => {
        println!("Blocked: {:?}", response.reason);
    }
    MitigationAction::Challenge => {
        if let Some(challenge) = response.challenge {
            println!("Challenge type: {}", challenge.challenge_type);
            println!("Challenge token: {}", challenge.token);
        }
    }
}

verify_challenge(token, response)

Verify a user’s challenge response.
let is_valid = client.verify_challenge(
    "challenge-token",
    "user-response"
)?;

if is_valid {
    println!("Challenge passed");
} else {
    println!("Challenge failed");
}

send_beacon_event(&event)

Send request traffic to the beacon server so Esper can analyze it and update state for later mitigation checks.
use esper_rs::BeaconEvent;

let event = BeaconEvent::new("page_view")
    .with_ip("192.168.1.1")
    .with_user_agent("Mozilla/5.0...")
    .with_session_id("session-123")
    .with_data("page", serde_json::json!("/home"));

client.send_beacon_event(&event)?;

get_usage()

Get API usage statistics.
let usage = client.get_usage()?;
println!("Total requests: {}", usage.requests);
println!("Blocked: {}", usage.blocked);
println!("Challenged: {}", usage.challenged);
println!("Allowed: {}", usage.allowed);

Framework Integration

Axum Middleware

use axum::{
    extract::{ConnectInfo, Request, State},
    http::StatusCode,
    middleware::{self, Next},
    response::{IntoResponse, Json, Response},
    routing::get,
    Router,
};
use esper_rs::{AsyncEsperClient, EsperConfig, MitigationRequest};
use std::{net::SocketAddr, sync::Arc};

#[derive(Clone)]
struct AppState {
    esper_client: Arc<AsyncEsperClient>,
}

async fn esper_middleware(
    State(state): State<AppState>,
    ConnectInfo(addr): ConnectInfo<SocketAddr>,
    request: Request,
    next: Next,
) -> Response {
    let mitigation_request = MitigationRequest::new(addr.ip().to_string())
        .with_path(request.uri().path().to_string())
        .with_method(request.method().to_string());

    match state.esper_client.check_mitigation(&mitigation_request).await {
        Ok(response) => match response.action {
            esper_rs::MitigationAction::Block => {
                (StatusCode::FORBIDDEN, "Access denied").into_response()
            }
            esper_rs::MitigationAction::Challenge => {
                (StatusCode::TOO_MANY_REQUESTS, "Challenge required")
                    .into_response()
            }
            esper_rs::MitigationAction::Allow => next.run(request).await,
        },
        Err(e) => {
            // Log error and allow on failure
            eprintln!("Esper error: {}", e);
            next.run(request).await
        }
    }
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let config = EsperConfig::new("your-api-key");
    let esper_client = Arc::new(AsyncEsperClient::new(config)?);
    let state = AppState { esper_client };

    let app = Router::new()
        .route("/", get(handler))
        .layer(middleware::from_fn_with_state(
            state.clone(),
            esper_middleware
        ))
        .with_state(state);

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
    axum::serve(listener, app).await?;

    Ok(())
}

Actix-web Middleware

use actix_web::{
    dev::{Service, ServiceRequest, ServiceResponse, Transform},
    error::ErrorForbidden,
    http::StatusCode,
    Error, HttpMessage, HttpResponse,
    web, App, HttpServer,
};
use esper_rs::{AsyncEsperClient, EsperConfig, MitigationRequest};
use futures_util::future::LocalBoxFuture;
use std::{
    future::{ready, Ready},
    rc::Rc,
};

pub struct EsperMiddleware {
    client: Rc<AsyncEsperClient>,
}

impl EsperMiddleware {
    pub fn new(api_key: &str) -> Self {
        let config = EsperConfig::new(api_key);
        let client = AsyncEsperClient::new(config)
            .expect("Failed to create Esper client");

        Self {
            client: Rc::new(client),
        }
    }
}

// Transform implementation
impl<S, B> Transform<S, ServiceRequest> for EsperMiddleware
where
    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
    S::Future: 'static,
    B: 'static,
{
    type Response = ServiceResponse<B>;
    type Error = Error;
    type InitError = ();
    type Transform = EsperMiddlewareService<S>;
    type Future = Ready<Result<Self::Transform, Self::InitError>>;

    fn new_transform(&self, service: S) -> Self::Future {
        ready(Ok(EsperMiddlewareService {
            service,
            client: self.client.clone(),
        }))
    }
}

pub struct EsperMiddlewareService<S> {
    service: S,
    client: Rc<AsyncEsperClient>,
}

// Service implementation
impl<S, B> Service<ServiceRequest> for EsperMiddlewareService<S>
where
    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
    S::Future: 'static,
    B: 'static,
{
    type Response = ServiceResponse<B>;
    type Error = Error;
    type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;

    fn poll_ready(
        &self,
        cx: &mut std::task::Context<'_>,
    ) -> std::task::Poll<Result<(), Self::Error>> {
        self.service.poll_ready(cx)
    }

    fn call(&self, req: ServiceRequest) -> Self::Future {
        let client = self.client.clone();
        let fut = self.service.call(req);

        Box::pin(async move {
            // Check mitigation
            let conn_info = req.connection_info();
            let mitigation_request = MitigationRequest::new(
                conn_info.realip_remote_addr()
                    .unwrap_or("127.0.0.1")
                    .to_string()
            )
            .with_path(req.path().to_string())
            .with_method(req.method().to_string());

            match client.check_mitigation(&mitigation_request).await {
                Ok(response) => match response.action {
                    esper_rs::MitigationAction::Block => {
                        Err(ErrorForbidden("Access denied"))
                    }
                    esper_rs::MitigationAction::Challenge => {
                        Err(actix_web::error::ErrorTooManyRequests(
                            "Challenge required"
                        ))
                    }
                    esper_rs::MitigationAction::Allow => fut.await,
                },
                Err(_) => fut.await, // Allow on error
            }
        })
    }
}

// Usage
#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .wrap(EsperMiddleware::new("your-api-key"))
            .route("/", web::get().to(handler))
    })
    .bind("127.0.0.1:8080")?
    .run()
    .await
}

Rocket Fairing

use rocket::{
    fairing::{Fairing, Info, Kind},
    http::Status,
    request::{self, FromRequest, Request},
    Data, Response, State,
};
use esper_rs::{AsyncEsperClient, EsperConfig, MitigationRequest};
use std::sync::Arc;

pub struct EsperFairing {
    client: Arc<AsyncEsperClient>,
}

impl EsperFairing {
    pub fn new(api_key: &str) -> Self {
        let config = EsperConfig::new(api_key);
        let client = AsyncEsperClient::new(config)
            .expect("Failed to create Esper client");

        Self {
            client: Arc::new(client),
        }
    }
}

#[rocket::async_trait]
impl Fairing for EsperFairing {
    fn info(&self) -> Info {
        Info {
            name: "Esper Traffic Protection",
            kind: Kind::Request | Kind::Response,
        }
    }

    async fn on_request(&self, req: &mut Request<'_>, _: &mut Data<'_>) {
        let ip = req.client_ip()
            .map(|ip| ip.to_string())
            .unwrap_or_else(|| "127.0.0.1".to_string());

        let mitigation_request = MitigationRequest::new(ip)
            .with_path(req.uri().path().to_string())
            .with_method(req.method().to_string());

        match self.client.check_mitigation(&mitigation_request).await {
            Ok(response) => match response.action {
                esper_rs::MitigationAction::Block => {
                    req.local_cache(|| EsperResult::Blocked);
                }
                esper_rs::MitigationAction::Challenge => {
                    req.local_cache(|| EsperResult::Challenged);
                }
                esper_rs::MitigationAction::Allow => {
                    req.local_cache(|| EsperResult::Allowed);
                }
            },
            Err(_) => {
                req.local_cache(|| EsperResult::Error);
            }
        }
    }
}

#[derive(Clone, Copy)]
enum EsperResult {
    Allowed,
    Blocked,
    Challenged,
    Error,
}

// Usage
#[rocket::launch]
fn rocket() -> _ {
    rocket::build()
        .attach(EsperFairing::new("your-api-key"))
        .mount("/", routes![index])
}

Error Handling

use esper_rs::Error;

match client.check_mitigation(&request) {
    Ok(response) => {
        // Handle response
    }
    Err(Error::ApiError { message, status_code, error_code }) => {
        eprintln!("API error: {} (status: {:?})", message, status_code);
    }
    Err(Error::TimeoutError) => {
        eprintln!("Request timed out");
    }
    Err(Error::ConnectionError(msg)) => {
        eprintln!("Connection error: {}", msg);
    }
    Err(e) => {
        eprintln!("Other error: {}", e);
    }
}

Features

The SDK supports different TLS implementations:
# Use rustls (default)
[dependencies]
esper-rs = "0.1.0"

# Use native TLS
[dependencies]
esper-rs = { version = "0.1.0", default-features = false, features = ["native-tls"] }

Testing

#[cfg(test)]
mod tests {
    use esper_rs::{EsperClient, EsperConfig, MitigationRequest};
    use mockito::{mock, Matcher};

    #[test]
    fn test_check_mitigation() {
        let mut server = mockito::Server::new();
        let url = server.url();

        let _m = server
            .mock("POST", "/api/v1/runtime/mitigation")
            .with_status(200)
            .with_header("content-type", "application/json")
            .with_body(r#"{"action": "allow", "score": 10.0}"#)
            .create();

        let config = EsperConfig::new("test-key")
            .with_api_url(url);
        let client = EsperClient::new(config).unwrap();

        let request = MitigationRequest::new("192.168.1.1");
        let response = client.check_mitigation(&request).unwrap();

        assert_eq!(response.action, esper_rs::MitigationAction::Allow);
    }

    #[tokio::test]
    async fn test_async_check_mitigation() {
        use esper_rs::AsyncEsperClient;

        let mut server = mockito::Server::new_async().await;
        let url = server.url();

        let _m = server
            .mock("POST", "/api/v1/runtime/mitigation")
            .with_status(200)
            .with_header("content-type", "application/json")
            .with_body(r#"{"action": "block", "reason": "Bot detected"}"#)
            .create_async()
            .await;

        let config = EsperConfig::new("test-key")
            .with_api_url(url);
        let client = AsyncEsperClient::new(config).unwrap();

        let request = MitigationRequest::new("10.0.0.1");
        let response = client.check_mitigation(&request).await.unwrap();

        assert_eq!(response.action, esper_rs::MitigationAction::Block);
    }
}

Performance

The Rust SDK is optimized for high performance:
  • Zero-copy deserialization with serde
  • Connection pooling with reqwest
  • Automatic retry with exponential backoff
  • Async/await support for concurrent operations

Examples

Complete examples are available in the GitHub repository:
  • Basic usage
  • Axum middleware
  • Actix-web middleware
  • Rocket fairing
  • Batch processing
  • Custom retry logic