Add proper error handling

This commit is contained in:
Tristan Daniël Maat 2022-09-06 15:14:42 +01:00
parent 98d71f0311
commit ab405fc1f8
Signed by: tlater
GPG key ID: 49670FD774E43268
4 changed files with 107 additions and 19 deletions

2
Cargo.lock generated
View file

@ -1215,9 +1215,11 @@ dependencies = [
"actix-files",
"actix-web",
"clap",
"derive_more",
"env_logger",
"handlebars",
"log",
"serde_json",
]
[[package]]

View file

@ -7,6 +7,8 @@ edition = "2021"
actix-files = "0.6.2"
actix-web = "4.1.0"
clap = { version = "3.2.17", features = ["derive"] }
derive_more = "0.99.17"
env_logger = "0.9.0"
handlebars = { version = "4.3.3", features = ["dir_source"] }
log = "0.4.17"
serde_json = "1.0.83"

76
src/errors.rs Normal file
View file

@ -0,0 +1,76 @@
use actix_web::body::BoxBody;
use actix_web::dev::ServiceResponse;
use actix_web::http::header::ContentType;
use actix_web::http::StatusCode;
use actix_web::middleware::ErrorHandlerResponse;
use actix_web::{web, HttpResponse, ResponseError};
use derive_more::{Display, Error};
use serde_json::json;
use super::SharedData;
#[derive(Debug, Display, Error)]
pub enum UserError {
// #[display(fmt = "The page could not be found.")]
NotFound,
#[display(fmt = "Internal error. Try again later.")]
InternalError,
}
impl ResponseError for UserError {
fn error_response(&self) -> HttpResponse {
HttpResponse::build(self.status_code())
.insert_header(ContentType::html())
.body(self.to_string())
}
fn status_code(&self) -> StatusCode {
match *self {
UserError::NotFound => StatusCode::NOT_FOUND,
UserError::InternalError => StatusCode::INTERNAL_SERVER_ERROR,
}
}
}
pub fn generic_error<B>(
res: ServiceResponse<B>,
) -> actix_web::Result<ErrorHandlerResponse<BoxBody>> {
let data = res
.request()
.app_data::<web::Data<SharedData>>()
.map(|t| t.get_ref());
let status_code = res.response().status();
let message = if let Some(error) = status_code.canonical_reason() {
error
} else {
""
};
let response = match data {
Some(SharedData {
handlebars,
config: _,
}) => {
let body = handlebars
.render(
"error",
&json!({
"message": message,
"status_code": status_code.as_u16()
}),
)
.map_err(|_| UserError::InternalError)?;
HttpResponse::build(res.status())
.content_type(ContentType::html())
.body(body)
}
None => Err(UserError::InternalError)?,
};
Ok(ErrorHandlerResponse::Response(ServiceResponse::new(
res.into_parts().0,
response.map_into_left_body(),
)))
}

View file

@ -1,10 +1,17 @@
#![allow(dead_code)]
use std::net::SocketAddr;
use std::path::PathBuf;
use actix_files::NamedFile;
use actix_web::http::StatusCode;
use actix_web::middleware::ErrorHandlers;
use actix_web::{get, web, App, HttpRequest, HttpResponse, HttpServer, Responder};
use clap::Parser;
use handlebars::Handlebars;
use std::path::PathBuf;
mod errors;
use errors::{generic_error, UserError};
#[derive(Parser, Debug, Clone)]
struct Config {
@ -21,16 +28,17 @@ struct Config {
dev_mode: bool,
}
#[derive(Debug)]
struct SharedData<'a> {
handlebars: Handlebars<'a>,
config: Config,
}
#[get("/{filename:.*.html}")]
#[get(r"/{filename:.*\.html}")]
async fn template(
shared: web::Data<SharedData<'_>>,
req: HttpRequest,
) -> Result<impl Responder, Box<dyn std::error::Error>> {
) -> actix_web::Result<impl Responder> {
let path = req
.match_info()
.query("filename")
@ -38,22 +46,21 @@ async fn template(
.expect("only paths with this suffix should get here");
if shared.handlebars.has_template(path) {
let body = shared.handlebars.render(path, &())?;
Ok(HttpResponse::Ok().body(body))
} else {
let body = shared
.handlebars
.render("404", &())
.expect("404 template not found");
Ok(HttpResponse::NotFound().body(body))
.render(path, &())
.map_err(|_| UserError::InternalError)?;
Ok(HttpResponse::Ok().body(body))
} else {
Err(UserError::NotFound)?
}
}
#[get("/{filename:.*}")]
#[get("/{filename:.*[^/]+}")]
async fn static_file(
shared: web::Data<SharedData<'_>>,
req: HttpRequest,
) -> Result<impl Responder, Box<dyn std::error::Error>> {
) -> actix_web::Result<impl Responder> {
let requested = req.match_info().query("filename");
match shared
@ -67,17 +74,13 @@ async fn static_file(
//
// i.e., don't serve up /etc/passwd
Ok(path) if path.starts_with(&shared.config.template_directory) => {
let file = NamedFile::open_async(path).await?;
let file = NamedFile::open_async(path)
.await
.map_err(|_| UserError::NotFound)?;
Ok(file.use_last_modified(false).respond_to(&req))
}
// Any other cases should 404
_ => {
let body = shared
.handlebars
.render("404", &())
.expect("404 template not found");
Ok(HttpResponse::NotFound().body(body))
}
_ => Err(UserError::NotFound)?,
}
}
@ -101,6 +104,11 @@ async fn main() -> Result<(), std::io::Error> {
HttpServer::new(move || {
App::new()
.wrap(
ErrorHandlers::new()
.handler(StatusCode::NOT_FOUND, generic_error)
.handler(StatusCode::INTERNAL_SERVER_ERROR, generic_error),
)
.app_data(shared_data.clone())
.service(template)
.service(static_file)