Improve error handling
This commit is contained in:
71
src/error.rs
71
src/error.rs
@@ -1,26 +1,67 @@
|
||||
use axum::{http::StatusCode, response::{IntoResponse, Response}};
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use axum::Json;
|
||||
use tracing::error;
|
||||
use serde_with::DisplayFromStr;
|
||||
use validator::ValidationErrors;
|
||||
|
||||
// Make our own error that wraps `anyhow::Error`.
|
||||
pub struct AppError(anyhow::Error);
|
||||
/// An API-friendly error type.
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum Error {
|
||||
/// A SQLx call returned an error.
|
||||
///
|
||||
/// The exact error contents are not reported to the user in order to avoid leaking
|
||||
/// information about database internals.
|
||||
#[error("an internal database error occurred")]
|
||||
Sqlx(#[from] sqlx::Error),
|
||||
|
||||
// Tell axum how to convert `AppError` into a response.
|
||||
impl IntoResponse for AppError {
|
||||
/// Similarly, we don't want to report random `anyhow` errors to the user.
|
||||
#[error("an internal server error occurred")]
|
||||
Anyhow(#[from] anyhow::Error),
|
||||
|
||||
#[error("validation error in request body")]
|
||||
InvalidEntity(#[from] ValidationErrors),
|
||||
}
|
||||
|
||||
impl IntoResponse for Error {
|
||||
fn into_response(self) -> Response {
|
||||
#[serde_with::serde_as]
|
||||
#[serde_with::skip_serializing_none]
|
||||
#[derive(serde::Serialize)]
|
||||
struct ErrorResponse<'a> {
|
||||
// Serialize the `Display` output as the error message
|
||||
#[serde_as(as = "DisplayFromStr")]
|
||||
message: &'a Error,
|
||||
|
||||
errors: Option<&'a ValidationErrors>,
|
||||
}
|
||||
|
||||
let errors = match &self {
|
||||
Error::InvalidEntity(errors) => Some(errors),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
error!("API error: {:?}", self);
|
||||
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Something went wrong: {}", self.0),
|
||||
self.status_code(),
|
||||
Json(ErrorResponse {
|
||||
message: &self,
|
||||
errors,
|
||||
}),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
}
|
||||
|
||||
// This enables using `?` on functions that return `Result<_, anyhow::Error>` to turn them into
|
||||
// `Result<_, AppError>`. That way you don't need to do that manually.
|
||||
impl<E> From<E> for AppError
|
||||
where
|
||||
E: Into<anyhow::Error>,
|
||||
{
|
||||
fn from(err: E) -> Self {
|
||||
Self(err.into())
|
||||
impl Error {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
use Error::*;
|
||||
|
||||
match self {
|
||||
Sqlx(sqlx::Error::RowNotFound) => StatusCode::NOT_FOUND,
|
||||
Sqlx(_) | Anyhow(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
InvalidEntity(_) => StatusCode::UNPROCESSABLE_ENTITY,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
use axum::{extract::{State, Path}, Json};
|
||||
use sqlx::PgPool;
|
||||
|
||||
use crate::error::AppError;
|
||||
use crate::error::Error;
|
||||
use crate::models::item::{create_item, get_item, CreateItem, Item};
|
||||
|
||||
pub async fn get(
|
||||
State(pool): State<PgPool>,
|
||||
Path(id): Path<i32>,
|
||||
) -> Result<Json<Item>, AppError> {
|
||||
) -> Result<Json<Item>, Error> {
|
||||
Ok(Json(get_item(pool, id).await?))
|
||||
}
|
||||
|
||||
pub async fn post(
|
||||
State(pool): State<PgPool>,
|
||||
Json(payload): Json<CreateItem>,
|
||||
) -> Result<Json<Item>, AppError> {
|
||||
) -> Result<Json<Item>, Error> {
|
||||
Ok(Json(create_item(pool, payload).await?))
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use axum::{extract::State, Json};
|
||||
use sqlx::PgPool;
|
||||
|
||||
use crate::error::AppError;
|
||||
use crate::error::Error;
|
||||
use crate::models::item::{get_items, Item};
|
||||
|
||||
pub async fn get(State(pool): State<PgPool>) -> Result<Json<Vec<Item>>, AppError> {
|
||||
pub async fn get(State(pool): State<PgPool>) -> Result<Json<Vec<Item>>, Error> {
|
||||
Ok(Json(get_items(pool).await?))
|
||||
}
|
||||
|
||||
@@ -27,9 +27,9 @@ async fn main() -> anyhow::Result<()> {
|
||||
sqlx::migrate!().run(&pool).await?;
|
||||
|
||||
let app = Router::new()
|
||||
.route("/items", get(handlers::items::get))
|
||||
.route("/item", post(handlers::item::post))
|
||||
.route("/item/:id", get(handlers::item::get))
|
||||
.route("/v1/items", get(handlers::items::get))
|
||||
.route("/v1/item", post(handlers::item::post))
|
||||
.route("/v1/item/:id", get(handlers::item::get))
|
||||
.with_state(pool)
|
||||
.layer(ServiceBuilder::new().layer(TraceLayer::new_for_http()));
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use anyhow::Context;
|
||||
use chrono::NaiveDateTime;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::PgPool;
|
||||
@@ -21,21 +20,19 @@ pub struct CreateItem {
|
||||
description: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn get_item(pool: PgPool, id: i32) -> anyhow::Result<Item> {
|
||||
pub async fn get_item(pool: PgPool, id: i32) -> sqlx::Result<Item> {
|
||||
sqlx::query_as!(Item, "SELECT * FROM items WHERE id = $1", id)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.context("Failed to fetch item")
|
||||
}
|
||||
|
||||
pub async fn get_items(pool: PgPool) -> anyhow::Result<Vec<Item>> {
|
||||
pub async fn get_items(pool: PgPool) -> sqlx::Result<Vec<Item>> {
|
||||
sqlx::query_as!(Item, "SELECT * FROM items")
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.context("Failed to fetch items")
|
||||
}
|
||||
|
||||
pub async fn create_item(pool: PgPool, payload: CreateItem) -> anyhow::Result<Item> {
|
||||
pub async fn create_item(pool: PgPool, payload: CreateItem) -> sqlx::Result<Item> {
|
||||
sqlx::query_as!(
|
||||
Item,
|
||||
"INSERT INTO items (
|
||||
@@ -49,5 +46,4 @@ pub async fn create_item(pool: PgPool, payload: CreateItem) -> anyhow::Result<It
|
||||
)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.context("Failed to create item")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user