rust 53 lines · 6 steps

Rate-limit errors as Axum responses

A custom error type implements IntoResponse so a throttled request returns 429 with a Retry-After header automatically.

Explained by highlit
1use std::time::Duration;
2 
3use axum::{
4 extract::State,
5 http::{header::RETRY_AFTER, StatusCode},
6 response::{IntoResponse, Response},
7 Json,
8};
9use serde_json::json;
10 
11#[derive(Clone)]
12struct AppState {
13 limiter: RateLimiter,
14}
15 
16enum QuotaError {
17 Exceeded { retry_after: Duration },
18}
19 
20impl IntoResponse for QuotaError {
21 fn into_response(self) -> Response {
22 match self {
23 QuotaError::Exceeded { retry_after } => {
24 let secs = retry_after.as_secs().max(1);
25 (
26 StatusCode::TOO_MANY_REQUESTS,
27 [(RETRY_AFTER, secs.to_string())],
28 Json(json!({
29 "error": "rate_limit_exceeded",
30 "message": "Quota exceeded, slow down.",
31 "retry_after": secs,
32 })),
33 )
34 .into_response()
35 }
36 }
37 }
38}
39 
40async fn create_message(
41 State(state): State<AppState>,
42 Json(payload): Json<NewMessage>,
43) -> Result<impl IntoResponse, QuotaError> {
44 match state.limiter.check(&payload.sender).await {
45 Verdict::Allowed => {}
46 Verdict::Throttled { reset_in } => {
47 return Err(QuotaError::Exceeded { retry_after: reset_in });
48 }
49 }
50 
51 let message = Message::persist(payload).await;
52 Ok((StatusCode::CREATED, Json(message)))
53}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Implementing IntoResponse lets a domain error type map itself to a full HTTP response.
  2. 2Returning Result<_, E> from a handler turns error branches into clean early returns.
  3. 3Setting Retry-After alongside a 429 gives clients an actionable signal for when to retry.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Rate-limit errors as Axum responses — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code