feat: implement server error handling

image-uploads
Jef Roosens 2025-01-08 09:35:46 +01:00
parent d7c5c85460
commit 3add93bdb2
No known key found for this signature in database
GPG Key ID: 21FD3D77D56BAF49
4 changed files with 113 additions and 33 deletions

View File

@ -10,17 +10,12 @@ pub fn app(ctx: crate::Context) -> axum::Router {
async fn post_comment(
State(ctx): State<crate::Context>,
Form(comment): Form<NewComment>,
) -> Html<String> {
) -> super::Result<Html<String>> {
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)?))
}

View File

@ -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<E: std::error::Error> ErrorExt for E {}
impl From<db::DbError> for AppError {
fn from(value: db::DbError) -> Self {
Self::Db(value)
}
}
impl From<tera::Error> 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()
}
}
}
}

View File

@ -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<T> = std::result::Result<T, error::AppError>;
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<crate::Context>) -> Html<String> {
async fn get_index(State(ctx): State<crate::Context>) -> Result<Html<String>> {
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)?))
}

View File

@ -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<crate::Context>,
headers: HeaderMap,
Path(plant_id): Path<i32>,
) -> Html<String> {
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<Html<String>> {
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<crate::Context>,
Form(plant): Form<db::NewPlant>,
) -> Html<String> {
) -> super::Result<Html<String>> {
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)?))
}