Render error pages in Axum
This morning I received an email from Google telling me one of my pages had index issues by rendering a 5xx.
When I visited the page it returned a "Page not found" database error, and while it's technically an error, there are better response codes for this scenario.
Time to implement proper error pages.
Site structure
This site is split up in a couple of crates, to keep compile times low (in theory).
. ├── api ├── bin ├── global └── web
The global
crate contains code to interact with the database, and the web
crate renders HTML.
Database errors
I use the thiserror crate to emit errors from my Global crate like this:
use thiserror::Error; #[derive(Debug, Error)] pub enum DataStoreError { #[error(transparent)] DieselError(#[from] diesel::result::Error), #[error("Database Pool Error: {0}")] PoolError(#[from] deadpool_diesel::PoolError), #[error("Database Result Error: {0}")] DeadpoolError(#[from] deadpool_diesel::Error), #[error("Migration error: {0}")] MigrationError(String), }
It converts the Diesel
and Deadpool
errors to a single enum without having to implement a bunch of traits.
The goal here is to render a 404 page for a NotFound
Diesel error, and render a 500 for the rest, so I Make the diesel::result::Error
Transparent to save me a bit of de-structuring later on.
HTTP errors
The goal for Http errors is to not have to write map_err
every time, but ideally I can just use the ?
syntax everywhere and it "Just works(tm)".
To do this we need to make sure Axum understands what to do with the raised errors. And this can be done by implementing IntoResponse
for the error enum.
My error enum looks like this;
pub enum AppError { NotFound, Unauthorized, Internal(anyhow::Error), }
And I implement IntoResponse
like so:
impl IntoResponse for AppError { fn into_response(self) -> Response { let (code, body) = match self { AppError::NotFound => (StatusCode::NOT_FOUND, not_found()), AppError::Unauthorized => (StatusCode::UNAUTHORIZED, unauthorized()), AppError::Internal(inner) => { tracing::error!( "Returning http error: 500, for error: {}", inner.to_string() ); (StatusCode::INTERNAL_SERVER_ERROR, internal_error(inner)) } }; (code, body).into_response() } }
I render HTML templates for each error and return the proper status code, depending on the error type.
Now that we can render error pages, we still need to cover the scenario that NotFound
errors render as a 404, and not a 500.
I do this by implementing the From
trait for the DataStoreError
into the AppError
.
impl From<DataStoreError> for AppError { fn from(inner: DataStoreError) -> Self { match inner { DataStoreError::DieselError(DieselError::NotFound) => Self::NotFound, _ => Self::Internal(inner.into()), } } }
And for good measure, I also do this for anyhow::Error
, to match pretty much every other error type.
impl From<anyhow::Error> for AppError { fn from(inner: anyhow::Error) -> Self { AppError::Internal(inner) } }
With these traits implemented, I can now return a Result
in my application code and Axum handles the rest.
pub async fn index(State(state): State<GlobalState>) -> Result<Markup, AppError> { let posts = state .store .list_pages_for_collection("blog_posts") .await?; #[...] }