Improve error handling
This commit is contained in:
parent
c2c0f7a28d
commit
de157a3b1e
225
Cargo.lock
generated
225
Cargo.lock
generated
@ -13,6 +13,15 @@ dependencies = [
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "android_system_properties"
|
||||
version = "0.1.5"
|
||||
@ -171,7 +180,7 @@ dependencies = [
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
"serde",
|
||||
"time",
|
||||
"time 0.1.45",
|
||||
"wasm-bindgen",
|
||||
"winapi",
|
||||
]
|
||||
@ -221,12 +230,15 @@ dependencies = [
|
||||
"dotenvy",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_with",
|
||||
"sqlx",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tower",
|
||||
"tower-http",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"validator",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -317,6 +329,41 @@ dependencies = [
|
||||
"syn 2.0.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.20.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0558d22a7b463ed0241e993f76f09f30b126687447751a8638587b864e4b3944"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"darling_macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_core"
|
||||
version = "0.20.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab8bfa2e259f8ee1ce5e97824a3c55ec4404a0d772ca7fa96bf19f0752a046eb"
|
||||
dependencies = [
|
||||
"fnv",
|
||||
"ident_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"syn 2.0.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_macro"
|
||||
version = "0.20.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29a358ff9f12ec09c3e61fef9b5a9902623a695a46a917b07f269bff1445611a"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"quote",
|
||||
"syn 2.0.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.6"
|
||||
@ -694,6 +741,23 @@ dependencies = [
|
||||
"cxx-build",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ident_case"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8"
|
||||
dependencies = [
|
||||
"matches",
|
||||
"unicode-bidi",
|
||||
"unicode-normalization",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "0.3.0"
|
||||
@ -704,6 +768,12 @@ dependencies = [
|
||||
"unicode-normalization",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "if_chain"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "1.9.3"
|
||||
@ -712,6 +782,7 @@ checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"hashbrown",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -810,6 +881,12 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "matches"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
|
||||
|
||||
[[package]]
|
||||
name = "matchit"
|
||||
version = "0.7.0"
|
||||
@ -1082,6 +1159,30 @@ version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
|
||||
dependencies = [
|
||||
"proc-macro-error-attr",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error-attr"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.56"
|
||||
@ -1159,6 +1260,23 @@ dependencies = [
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af83e617f331cc6ae2da5443c602dfa5af81e517212d9d611a5b3ba1777b5370"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a5996294f19bd3aae0453a862ad728f60e6600695733dd5df01da90c54363a3c"
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.11.17"
|
||||
@ -1318,6 +1436,34 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_with"
|
||||
version = "3.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f02d8aa6e3c385bf084924f660ce2a3a6bd333ba55b35e8590b321f35d88513"
|
||||
dependencies = [
|
||||
"base64 0.21.0",
|
||||
"chrono",
|
||||
"hex",
|
||||
"indexmap",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_with_macros",
|
||||
"time 0.3.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_with_macros"
|
||||
version = "3.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "edc7d5d3932fb12ce722ee5e64dd38c504efba37567f0c402f6ca728c3b8b070"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha1"
|
||||
version = "0.10.5"
|
||||
@ -1496,6 +1642,12 @@ dependencies = [
|
||||
"unicode-normalization",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.4.1"
|
||||
@ -1593,6 +1745,33 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f3403384eaacbca9923fa06940178ac13e4edb725486d70e8e15881d0c836cc"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"serde",
|
||||
"time-core",
|
||||
"time-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time-core"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb"
|
||||
|
||||
[[package]]
|
||||
name = "time-macros"
|
||||
version = "0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "372950940a5f07bf38dbe211d7283c9e6d7327df53794992d293e534c733d09b"
|
||||
dependencies = [
|
||||
"time-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec"
|
||||
version = "1.6.0"
|
||||
@ -1837,10 +2016,52 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"idna",
|
||||
"idna 0.3.0",
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "validator"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32ad5bf234c7d3ad1042e5252b7eddb2c4669ee23f32c7dd0e9b7705f07ef591"
|
||||
dependencies = [
|
||||
"idna 0.2.3",
|
||||
"lazy_static",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"url",
|
||||
"validator_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "validator_derive"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc44ca3088bb3ba384d9aecf40c6a23a676ce23e09bdaca2073d99c207f864af"
|
||||
dependencies = [
|
||||
"if_chain",
|
||||
"lazy_static",
|
||||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
"syn 1.0.109",
|
||||
"validator_types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "validator_types"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "111abfe30072511849c5910134e8baf8dc05de4c0e5903d681cbd5c9c4d611e3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
version = "0.1.0"
|
||||
|
@ -12,9 +12,12 @@ chrono = { version = "0.4", features = ["serde"] }
|
||||
dotenvy = "0.15"
|
||||
reqwest = { version = "0.11", features = ["json"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_with = "3"
|
||||
sqlx = { version = "0.6", features = ["runtime-tokio-native-tls", "postgres", "macros", "migrate", "chrono"] }
|
||||
thiserror = "1"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tower = "0.4"
|
||||
tower-http = { version = "0.4", features = ["trace"] }
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = "0.3"
|
||||
validator = { version = "0.16", features = ["derive"] }
|
||||
|
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")
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user