rust 69 lines · 7 steps

Turning errors into RFC 7807 responses in Axum

An ApiError enum implements IntoResponse to render every failure as a structured problem+json payload with the right status code.

Explained by highlit
1use axum::{
2 http::{header, StatusCode},
3 response::{IntoResponse, Response},
4 Json,
5};
6use serde::Serialize;
7use serde_json::json;
8 
9#[derive(Debug, thiserror::Error)]
10pub enum ApiError {
11 #[error("resource not found")]
12 NotFound,
13 #[error("validation failed")]
14 Validation(Vec<String>),
15 #[error("insufficient permissions")]
16 Forbidden,
17 #[error("internal server error")]
18 Internal(#[from] anyhow::Error),
19}
20 
21#[derive(Serialize)]
22struct ProblemDetails {
23 #[serde(rename = "type")]
24 type_uri: &'static str,
25 title: &'static str,
26 status: u16,
27 detail: String,
28 #[serde(skip_serializing_if = "Vec::is_empty")]
29 errors: Vec<String>,
30}
31 
32impl IntoResponse for ApiError {
33 fn into_response(self) -> Response {
34 let (status, type_uri, title, errors) = match &self {
35 ApiError::NotFound => (StatusCode::NOT_FOUND, "about:blank", "Not Found", vec![]),
36 ApiError::Validation(e) => (
37 StatusCode::UNPROCESSABLE_ENTITY,
38 "https://errors.example.com/validation",
39 "Validation Failed",
40 e.clone(),
41 ),
42 ApiError::Forbidden => (StatusCode::FORBIDDEN, "about:blank", "Forbidden", vec![]),
43 ApiError::Internal(err) => {
44 tracing::error!(error = ?err, "unhandled internal error");
45 (
46 StatusCode::INTERNAL_SERVER_ERROR,
47 "about:blank",
48 "Internal Server Error",
49 vec![],
50 )
51 }
52 };
53 
54 let body = Json(ProblemDetails {
55 type_uri,
56 title,
57 status: status.as_u16(),
58 detail: self.to_string(),
59 errors,
60 });
61 
62 (
63 status,
64 [(header::CONTENT_TYPE, "application/problem+json")],
65 body,
66 )
67 .into_response()
68 }
69}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Implementing IntoResponse lets a domain error type become a first-class HTTP response, so handlers can just return it.
  2. 2Matching on the error variant centralizes status codes and messages in one place instead of scattering them across handlers.
  3. 3The RFC 7807 problem+json shape gives clients a consistent, machine-readable error contract.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Turning errors into RFC 7807 responses in Axum — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code