From 3add93bdb2acb82a183ee1b154b1ab7e33852a07 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Wed, 8 Jan 2025 09:35:46 +0100 Subject: [PATCH] feat: implement server error handling --- src/server/comments.rs | 11 ++----- src/server/error.rs | 75 ++++++++++++++++++++++++++++++++++++++++++ src/server/mod.rs | 10 +++--- src/server/plants.rs | 50 ++++++++++++++++------------ 4 files changed, 113 insertions(+), 33 deletions(-) create mode 100644 src/server/error.rs diff --git a/src/server/comments.rs b/src/server/comments.rs index ad41a44..ca9f889 100644 --- a/src/server/comments.rs +++ b/src/server/comments.rs @@ -10,17 +10,12 @@ pub fn app(ctx: crate::Context) -> axum::Router { async fn post_comment( State(ctx): State, Form(comment): Form, -) -> Html { +) -> super::Result> { let comment = tokio::task::spawn_blocking(move || comment.insert(&ctx.pool)) .await - .unwrap() - .unwrap(); + .unwrap()?; let mut context = Context::new(); context.insert("comment", &comment); - Html( - ctx.tera - .render("partials/comment_li.html", &context) - .unwrap(), - ) + Ok(Html(ctx.tera.render("partials/comment_li.html", &context)?)) } diff --git a/src/server/error.rs b/src/server/error.rs new file mode 100644 index 0000000..1d8049c --- /dev/null +++ b/src/server/error.rs @@ -0,0 +1,75 @@ +use std::fmt::{self, Write}; + +use axum::{http::StatusCode, response::IntoResponse}; + +use crate::db; + +#[derive(Debug)] +pub enum AppError { + Db(db::DbError), + Tera(tera::Error), + NotFound, +} + +impl fmt::Display for AppError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Db(_) => write!(f, "database error"), + Self::Tera(_) => write!(f, "error rendering template"), + Self::NotFound => write!(f, "not found"), + } + } +} + +impl std::error::Error for AppError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::Db(err) => Some(err), + Self::Tera(err) => Some(err), + Self::NotFound => None, + } + } +} + +pub trait ErrorExt: std::error::Error { + /// Return the full chain of error messages + fn stack(&self) -> String { + let mut msg = format!("{}", self); + let mut err = self.source(); + + while let Some(src) = err { + write!(msg, " - {}", src).unwrap(); + + err = src.source(); + } + + msg + } +} + +impl ErrorExt for E {} + +impl From for AppError { + fn from(value: db::DbError) -> Self { + Self::Db(value) + } +} + +impl From for AppError { + fn from(value: tera::Error) -> Self { + Self::Tera(value) + } +} + +impl IntoResponse for AppError { + fn into_response(self) -> axum::response::Response { + match self { + Self::NotFound => StatusCode::NOT_FOUND.into_response(), + _ => { + tracing::error!("{}", self.stack()); + + StatusCode::INTERNAL_SERVER_ERROR.into_response() + } + } + } +} diff --git a/src/server/mod.rs b/src/server/mod.rs index 02c77f0..de2ecb8 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -1,4 +1,5 @@ mod comments; +mod error; mod plants; use axum::{ @@ -13,6 +14,8 @@ use tower_http::set_header::SetResponseHeaderLayer; use crate::db::Plant; +pub type Result = std::result::Result; + const HX_REQUEST_HEADER: &str = "HX-Request"; const HX_HISTORY_RESTORE_HEADER: &str = "HX-History-Restore-Request"; @@ -47,13 +50,12 @@ pub fn app(ctx: crate::Context) -> axum::Router { )) } -async fn get_index(State(ctx): State) -> Html { +async fn get_index(State(ctx): State) -> Result> { let plants = tokio::task::spawn_blocking(move || Plant::all(&ctx.pool)) .await - .unwrap() - .unwrap(); + .unwrap()?; let mut context = Context::new(); context.insert("plants", &plants); - Html(ctx.tera.render("index.html", &context).unwrap()) + Ok(Html(ctx.tera.render("index.html", &context)?)) } diff --git a/src/server/plants.rs b/src/server/plants.rs index f2839a8..279d3e9 100644 --- a/src/server/plants.rs +++ b/src/server/plants.rs @@ -9,7 +9,7 @@ use tera::Context; use crate::db::{self, DbError, Plant}; -use super::render_partial; +use super::{error::AppError, render_partial}; pub fn app(ctx: crate::Context) -> axum::Router { Router::new() @@ -22,40 +22,48 @@ async fn get_plant_page( State(ctx): State, headers: HeaderMap, Path(plant_id): Path, -) -> Html { - let (plant, comments) = tokio::task::spawn_blocking(move || { - let plant = Plant::by_id(&ctx.pool, plant_id)?.unwrap(); - let comments = plant.comments(&ctx.pool)?; +) -> super::Result> { + let res = tokio::task::spawn_blocking(move || { + let plant = Plant::by_id(&ctx.pool, plant_id)?; - Ok::<_, DbError>((plant, comments)) + if let Some(plant) = plant { + let comments = plant.comments(&ctx.pool)?; + + Ok::<_, DbError>(Some((plant, comments))) + } else { + Ok(None) + } }) .await - .unwrap() - .unwrap(); + .unwrap()?; - let mut context = Context::new(); - context.insert("plant", &plant); - context.insert("comments", &comments); + match res { + Some((plant, comments)) => { + let mut context = Context::new(); + context.insert("plant", &plant); + context.insert("comments", &comments); - let tmpl = if render_partial(&headers) { - "partials/plant_info.html" - } else { - "plant_page.html" - }; + let tmpl = if render_partial(&headers) { + "partials/plant_info.html" + } else { + "plant_page.html" + }; - Html(ctx.tera.render(tmpl, &context).unwrap()) + Ok(Html(ctx.tera.render(tmpl, &context)?)) + } + None => Err(AppError::NotFound), + } } async fn post_plant( State(ctx): State, Form(plant): Form, -) -> Html { +) -> super::Result> { let plant = tokio::task::spawn_blocking(move || plant.insert(&ctx.pool)) .await - .unwrap() - .unwrap(); + .unwrap()?; let mut context = Context::new(); context.insert("plant", &plant); - Html(ctx.tera.render("partials/plant_li.html", &context).unwrap()) + Ok(Html(ctx.tera.render("partials/plant_li.html", &context)?)) }