Webhooks Module
Webhook orchestration for sending and receiving HTTP callbacks in the Armature framework.
Table of Contents
- Overview
- Features
- Installation
- Sending Webhooks
- Receiving Webhooks
- Endpoint Registry
- Signature Security
- Retry Policies
- Best Practices
- API Reference
- Summary
Overview
The armature-webhooks module provides comprehensive webhook support including:
- Outgoing Webhooks: Send HTTP callbacks to registered endpoints
- Incoming Webhooks: Receive and verify webhooks from external services
- Signature Verification: HMAC-SHA256 signing and verification
- Automatic Retries: Configurable retry policies with exponential backoff
- Event System: Subscribe endpoints to specific event types
Features
- โ Send webhooks with automatic HMAC-SHA256 signatures
- โ Receive and verify incoming webhooks
- โ Event-based endpoint subscriptions with wildcards
- โ Configurable retry policies (fixed, exponential backoff)
- โ Delivery tracking and status monitoring
- โ Endpoint registry with failure tracking
- โ Timestamp validation to prevent replay attacks
- โ Custom headers per endpoint
Installation
Add the webhooks feature to your Cargo.toml:
[dependencies]
armature-framework = { version = "0.1", features = ["webhooks"] }
Or use the crate directly:
[dependencies]
armature-webhooks = "0.1"
Sending Webhooks
Basic Usage
use armature_webhooks::{WebhookClient, WebhookConfig, WebhookPayload};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = WebhookClient::new(WebhookConfig::default());
let payload = WebhookPayload::new("user.created")
.with_data(serde_json::json!({
"user_id": "usr_123",
"email": "user@example.com"
}));
// Send to a specific URL
let delivery = client.send("https://example.com/webhook", payload).await?;
println!("Delivery status: {:?}", delivery.status);
Ok(())
}
With Signing Secret
let delivery = client
.send_with_secret(
"https://example.com/webhook",
payload,
Some("your-signing-secret"),
)
.await?;
Dispatching to Multiple Endpoints
use std::sync::Arc;
let registry = Arc::new(WebhookRegistry::new());
// Register endpoints
registry.register(
WebhookEndpoint::builder("https://api1.example.com/webhook")
.events(vec!["user.*"])
.build()
);
registry.register(
WebhookEndpoint::builder("https://api2.example.com/webhook")
.events(vec!["user.created", "order.*"])
.build()
);
let client = WebhookClient::with_registry(WebhookConfig::default(), registry);
// Dispatch to all subscribed endpoints
let payload = WebhookPayload::new("user.created")
.with_data(serde_json::json!({"user_id": "123"}));
let deliveries = client.dispatch(payload).await?;
println!("Sent to {} endpoints", deliveries.len());
Receiving Webhooks
Verify and Parse
use armature_webhooks::WebhookReceiver;
let receiver = WebhookReceiver::new("your-signing-secret");
// In your HTTP handler
fn handle_webhook(body: &[u8], signature_header: &str) -> Result<(), WebhookError> {
// Verify and parse in one step
let payload = receiver.receive(body, signature_header)?;
println!("Received event: {}", payload.event);
println!("Data: {:?}", payload.data);
Ok(())
}
Verify from HTTP Headers
use std::collections::HashMap;
let mut headers = HashMap::new();
headers.insert("X-Webhook-Signature".to_string(), signature.to_string());
let is_valid = receiver.verify_from_headers(&body, &headers)?;
Event Handlers
let handler = receiver.handler("user.*", |payload| {
println!("Received user event: {}", payload.event);
// Process the webhook
Ok(())
});
// Handle incoming request
let handled = handler.handle(&body, &signature)?;
Endpoint Registry
Creating and Managing Endpoints
use armature_webhooks::{WebhookRegistry, WebhookEndpoint};
let registry = WebhookRegistry::new();
// Create an endpoint with builder
let endpoint = WebhookEndpoint::builder("https://api.example.com/webhooks")
.events(vec!["user.created", "user.updated", "order.*"])
.description("Main API webhook")
.header("X-Custom-Header", "custom-value")
.build();
let endpoint_id = registry.register(endpoint);
// Query endpoints
let user_endpoints = registry.get_endpoints_for_event("user.created");
let all_endpoints = registry.get_all();
// Manage endpoints
registry.enable(&endpoint_id)?;
registry.disable(&endpoint_id)?;
registry.unregister(&endpoint_id);
Event Wildcards
// Subscribe to all user events
let endpoint = WebhookEndpoint::builder("https://example.com")
.events(vec!["user.*"])
.build();
// Subscribe to all events
let endpoint = WebhookEndpoint::builder("https://example.com")
.all_events()
.build();
// Subscribe to specific events only
let endpoint = WebhookEndpoint::builder("https://example.com")
.events(vec!["user.created", "order.completed"])
.build();
Failure Tracking
// Record delivery results
registry.record_success(&endpoint_id)?;
registry.record_failure(&endpoint_id)?;
// Find problematic endpoints
let failing = registry.get_failing_endpoints(5); // โฅ5 consecutive failures
// Auto-disable failing endpoints
if endpoint.failure_count >= 10 {
registry.disable(&endpoint.id)?;
}
Signature Security
How Signatures Work
Webhooks are signed using HMAC-SHA256:
- A timestamp is included to prevent replay attacks
- The signature covers both timestamp and payload
- Format:
t=1234567890,v1=<hex-encoded-signature>
Signature Format
X-Webhook-Signature: t=1234567890,v1=abc123def456...
Where:
t= Unix timestamp when signature was createdv1= HMAC-SHA256 signature (hex encoded)
Timestamp Validation
// Default: 5 minutes tolerance
let receiver = WebhookReceiver::new("secret");
// Custom tolerance
let receiver = WebhookReceiver::new("secret")
.with_tolerance(60); // 60 seconds
Secret Rotation
let mut endpoint = registry.get(&endpoint_id).unwrap();
// Rotate to a new secret
let new_secret = endpoint.rotate_secret();
println!("New secret: {}", new_secret);
// Update in registry
registry.update(&endpoint_id, endpoint)?;
Retry Policies
Available Policies
use armature_webhooks::RetryPolicy;
use std::time::Duration;
// No retries
let policy = RetryPolicy::none();
// Fixed delay between retries
let policy = RetryPolicy::fixed(3, Duration::from_secs(5));
// Retries: 5s, 5s, 5s
// Exponential backoff (default)
let policy = RetryPolicy::exponential(5);
// Retries: 1s, 2s, 4s, 8s, 16s (capped at max_delay)
// Custom configuration
let policy = RetryPolicy {
max_attempts: 5,
initial_delay: Duration::from_secs(2),
max_delay: Duration::from_secs(120),
backoff_multiplier: 3.0,
jitter: true,
};
Configuring the Client
let config = WebhookConfig::builder()
.retry_policy(RetryPolicy::exponential(5))
.timeout_secs(30)
.build();
let client = WebhookClient::new(config);
Retryable Status Codes
The client automatically retries on these HTTP status codes:
408- Request Timeout429- Too Many Requests500- Internal Server Error502- Bad Gateway503- Service Unavailable504- Gateway Timeout
Best Practices
1. Always Verify Signatures
// โ Bad - no verification
let payload: WebhookPayload = serde_json::from_slice(&body)?;
// โ
Good - verify first
let payload = receiver.receive(&body, &signature)?;
2. Use Idempotency
// Store the webhook ID to prevent duplicate processing
let payload = receiver.receive(&body, &signature)?;
if already_processed(&payload.id) {
return Ok(()); // Skip duplicate
}
process_webhook(&payload)?;
mark_processed(&payload.id);
3. Return Quickly
// โ Bad - slow processing blocks response
let payload = receiver.receive(&body, &signature)?;
heavy_processing(&payload)?; // Takes 30 seconds
// โ
Good - queue for async processing
let payload = receiver.receive(&body, &signature)?;
queue.enqueue(payload)?;
Ok(()) // Return 200 immediately
4. Handle Failures Gracefully
let delivery = client.send(url, payload).await?;
match delivery.status {
WebhookDeliveryStatus::Succeeded => {
log::info!("Webhook delivered: {}", delivery.id);
}
WebhookDeliveryStatus::PermanentlyFailed => {
log::error!("Webhook failed after {} attempts", delivery.attempts);
// Alert, disable endpoint, etc.
}
_ => {
// Pending or in-progress
}
}
Common Pitfalls
- โ Don't trust unverified webhooks
- โ Don't process webhooks synchronously if they take time
- โ Don't ignore timestamp validation (replay attacks)
- โ Do implement idempotency
- โ Do return 200 quickly and process asynchronously
- โ Do monitor and alert on failing endpoints
API Reference
WebhookClient
impl WebhookClient {
fn new(config: WebhookConfig) -> Self;
fn with_registry(config: WebhookConfig, registry: Arc<WebhookRegistry>) -> Self;
async fn send(&self, url: &str, payload: WebhookPayload) -> Result<WebhookDelivery>;
async fn send_with_secret(&self, url: &str, payload: WebhookPayload, secret: Option<&str>) -> Result<WebhookDelivery>;
async fn send_to_endpoint(&self, endpoint: &WebhookEndpoint, payload: WebhookPayload) -> Result<WebhookDelivery>;
async fn dispatch(&self, payload: WebhookPayload) -> Result<Vec<WebhookDelivery>>;
}
WebhookReceiver
impl WebhookReceiver {
fn new(secret: impl Into<String>) -> Self;
fn with_tolerance(self, seconds: u64) -> Self;
fn verify(&self, payload: &[u8], signature: &str) -> Result<bool>;
fn receive(&self, payload: &[u8], signature: &str) -> Result<WebhookPayload>;
}
WebhookPayload
impl WebhookPayload {
fn new(event: impl Into<String>) -> Self;
fn with_data(self, data: serde_json::Value) -> Self;
fn with_metadata(self, metadata: serde_json::Value) -> Self;
fn to_bytes(&self) -> Result<Vec<u8>>;
fn to_json(&self) -> Result<String>;
}
WebhookEndpoint
impl WebhookEndpoint {
fn new(url: impl Into<String>) -> Self;
fn builder(url: impl Into<String>) -> WebhookEndpointBuilder;
fn is_subscribed_to(&self, event: &str) -> bool;
fn rotate_secret(&mut self) -> String;
}
WebhookRegistry
impl WebhookRegistry {
fn new() -> Self;
fn register(&self, endpoint: WebhookEndpoint) -> String;
fn unregister(&self, id: &str) -> Option<WebhookEndpoint>;
fn get(&self, id: &str) -> Option<WebhookEndpoint>;
fn get_endpoints_for_event(&self, event: &str) -> Vec<WebhookEndpoint>;
fn enable(&self, id: &str) -> Result<()>;
fn disable(&self, id: &str) -> Result<()>;
fn record_success(&self, id: &str) -> Result<()>;
fn record_failure(&self, id: &str) -> Result<()>;
}
Summary
Key Points:
- Use
WebhookClientto send outgoing webhooks - Use
WebhookReceiverto verify incoming webhooks - Use
WebhookRegistryto manage multiple endpoints - Always verify signatures before processing
- Implement idempotency for reliable processing
- Configure appropriate retry policies
Quick Start:
use armature_webhooks::{WebhookClient, WebhookConfig, WebhookPayload};
// Send a webhook
let client = WebhookClient::new(WebhookConfig::default());
let payload = WebhookPayload::new("user.created")
.with_data(json!({"user_id": "123"}));
client.send("https://example.com/webhook", payload).await?;
// Receive a webhook
let receiver = WebhookReceiver::new("secret");
let payload = receiver.receive(&body, &signature)?;