rust 53 lines · 9 steps

A concurrent webhook dispatcher in Axum

An Axum handler routes incoming webhooks to registered handlers and runs them all concurrently.

Explained by highlit
1use std::collections::HashMap;
2use std::sync::Arc;
3 
4use axum::body::Bytes;
5use axum::extract::State;
6use axum::http::{HeaderMap, StatusCode};
7use futures::future::join_all;
8use serde_json::Value;
9 
10#[async_trait::async_trait]
11pub trait WebhookHandler: Send + Sync {
12 async fn handle(&self, payload: &Value) -> anyhow::Result<()>;
13}
14 
15#[derive(Clone)]
16pub struct Dispatcher {
17 handlers: Arc<HashMap<String, Vec<Arc<dyn WebhookHandler>>>>,
18}
19 
20impl Dispatcher {
21 fn for_event(&self, event: &str) -> &[Arc<dyn WebhookHandler>] {
22 self.handlers.get(event).map(Vec::as_slice).unwrap_or(&[])
23 }
24}
25 
26pub async fn receive_webhook(
27 State(dispatcher): State<Dispatcher>,
28 headers: HeaderMap,
29 body: Bytes,
30) -> Result<StatusCode, (StatusCode, String)> {
31 let event = headers
32 .get("x-event-type")
33 .and_then(|v| v.to_str().ok())
34 .ok_or((StatusCode::BAD_REQUEST, "missing x-event-type header".into()))?;
35 
36 let payload: Value = serde_json::from_slice(&body)
37 .map_err(|e| (StatusCode::BAD_REQUEST, format!("invalid json: {e}")))?;
38 
39 let handlers = dispatcher.for_event(event);
40 if handlers.is_empty() {
41 tracing::warn!(%event, "no handlers registered for event");
42 return Ok(StatusCode::ACCEPTED);
43 }
44 
45 let results = join_all(handlers.iter().map(|h| h.handle(&payload))).await;
46 for result in results {
47 if let Err(err) = result {
48 tracing::error!(%event, error = %err, "downstream handler failed");
49 }
50 }
51 
52 Ok(StatusCode::OK)
53}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Boxing handlers behind a trait object lets you register any number of implementations under one event key.
  2. 2join_all turns a collection of futures into a single awaitable that runs them concurrently.
  3. 3Returning early with ACCEPTED for unknown events keeps a webhook endpoint forgiving instead of failing senders.

Related explainers

Share this explainer

Here's the card — post it anywhere.

A concurrent webhook dispatcher in Axum — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code