feat: implement server error handling
parent
d7c5c85460
commit
3add93bdb2
|
@ -10,17 +10,12 @@ pub fn app(ctx: crate::Context) -> axum::Router {
|
||||||
async fn post_comment(
|
async fn post_comment(
|
||||||
State(ctx): State<crate::Context>,
|
State(ctx): State<crate::Context>,
|
||||||
Form(comment): Form<NewComment>,
|
Form(comment): Form<NewComment>,
|
||||||
) -> Html<String> {
|
) -> super::Result<Html<String>> {
|
||||||
let comment = tokio::task::spawn_blocking(move || comment.insert(&ctx.pool))
|
let comment = tokio::task::spawn_blocking(move || comment.insert(&ctx.pool))
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()?;
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let mut context = Context::new();
|
let mut context = Context::new();
|
||||||
context.insert("comment", &comment);
|
context.insert("comment", &comment);
|
||||||
Html(
|
Ok(Html(ctx.tera.render("partials/comment_li.html", &context)?))
|
||||||
ctx.tera
|
|
||||||
.render("partials/comment_li.html", &context)
|
|
||||||
.unwrap(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
mod comments;
|
mod comments;
|
||||||
|
mod error;
|
||||||
mod plants;
|
mod plants;
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
|
@ -13,6 +14,8 @@ use tower_http::set_header::SetResponseHeaderLayer;
|
||||||
|
|
||||||
use crate::db::Plant;
|
use crate::db::Plant;
|
||||||
|
|
||||||
|
pub type Result<T> = std::result::Result<T, error::AppError>;
|
||||||
|
|
||||||
const HX_REQUEST_HEADER: &str = "HX-Request";
|
const HX_REQUEST_HEADER: &str = "HX-Request";
|
||||||
const HX_HISTORY_RESTORE_HEADER: &str = "HX-History-Restore-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))
|
let plants = tokio::task::spawn_blocking(move || Plant::all(&ctx.pool))
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()?;
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let mut context = Context::new();
|
let mut context = Context::new();
|
||||||
context.insert("plants", &plants);
|
context.insert("plants", &plants);
|
||||||
Html(ctx.tera.render("index.html", &context).unwrap())
|
Ok(Html(ctx.tera.render("index.html", &context)?))
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ use tera::Context;
|
||||||
|
|
||||||
use crate::db::{self, DbError, Plant};
|
use crate::db::{self, DbError, Plant};
|
||||||
|
|
||||||
use super::render_partial;
|
use super::{error::AppError, render_partial};
|
||||||
|
|
||||||
pub fn app(ctx: crate::Context) -> axum::Router {
|
pub fn app(ctx: crate::Context) -> axum::Router {
|
||||||
Router::new()
|
Router::new()
|
||||||
|
@ -22,17 +22,23 @@ async fn get_plant_page(
|
||||||
State(ctx): State<crate::Context>,
|
State(ctx): State<crate::Context>,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Path(plant_id): Path<i32>,
|
Path(plant_id): Path<i32>,
|
||||||
) -> Html<String> {
|
) -> super::Result<Html<String>> {
|
||||||
let (plant, comments) = tokio::task::spawn_blocking(move || {
|
let res = tokio::task::spawn_blocking(move || {
|
||||||
let plant = Plant::by_id(&ctx.pool, plant_id)?.unwrap();
|
let plant = Plant::by_id(&ctx.pool, plant_id)?;
|
||||||
|
|
||||||
|
if let Some(plant) = plant {
|
||||||
let comments = plant.comments(&ctx.pool)?;
|
let comments = plant.comments(&ctx.pool)?;
|
||||||
|
|
||||||
Ok::<_, DbError>((plant, comments))
|
Ok::<_, DbError>(Some((plant, comments)))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()?;
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
|
match res {
|
||||||
|
Some((plant, comments)) => {
|
||||||
let mut context = Context::new();
|
let mut context = Context::new();
|
||||||
context.insert("plant", &plant);
|
context.insert("plant", &plant);
|
||||||
context.insert("comments", &comments);
|
context.insert("comments", &comments);
|
||||||
|
@ -43,19 +49,21 @@ async fn get_plant_page(
|
||||||
"plant_page.html"
|
"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(
|
async fn post_plant(
|
||||||
State(ctx): State<crate::Context>,
|
State(ctx): State<crate::Context>,
|
||||||
Form(plant): Form<db::NewPlant>,
|
Form(plant): Form<db::NewPlant>,
|
||||||
) -> Html<String> {
|
) -> super::Result<Html<String>> {
|
||||||
let plant = tokio::task::spawn_blocking(move || plant.insert(&ctx.pool))
|
let plant = tokio::task::spawn_blocking(move || plant.insert(&ctx.pool))
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()?;
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let mut context = Context::new();
|
let mut context = Context::new();
|
||||||
context.insert("plant", &plant);
|
context.insert("plant", &plant);
|
||||||
Html(ctx.tera.render("partials/plant_li.html", &context).unwrap())
|
Ok(Html(ctx.tera.render("partials/plant_li.html", &context)?))
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue