//! error handling //! //! The [`RuntimeError`] enum is meant to be able to represent any error //! that might occur, well, at runtime. //! //! [`PresentableRuntimeError`] implements axum's `IntoResponse` so //! that it can use `fmt::Display` for creating it's error text. //! [`RuntimeError`] also implements `fmt::Display`, but this is used for //! logged errors, and it does not implement `IntoResponse` itself use std::{borrow::Cow, fmt, io::ErrorKind, sync::Arc}; use axum::{ body::Body, response::{IntoResponse, Response}, }; use camino::Utf8PathBuf; use gny::sternum::Sternum; use somethings::DatabaseError; use wikarden::WikError; use crate::QueryParseError; #[derive(Copy, Clone, Debug, PartialEq, Sternum)] pub enum InvalidPathReason { OutsideRoot, NotFound, } #[derive(Copy, Clone, Debug, PartialEq, Sternum)] pub enum FileErrorReason { PermissionDenied, NotFound, Unknown, } #[derive(Copy, Clone, Debug, PartialEq, Sternum)] pub enum TemplateErrorReason { NotATemplate, ParseError, } #[derive(Debug)] pub enum RuntimeError { /// The requested path was invalid. Often returned by [`RequestPath::verify_safe()`] InvalidPath { reason: InvalidPathReason, request_path: String, system_path: Utf8PathBuf, }, /// There was an error reading a file FileError { reason: FileErrorReason, source: std::io::Error, system_path: Utf8PathBuf, }, /// There was an error constructing a page from it's template TemplateError { reason: TemplateErrorReason, source: Option, system_path: Utf8PathBuf, }, /// Error parsing query parameters QueryParseError { source: QueryParseError }, /// Some field was missing in a form. /// Use [`RuntimeError::GenericBadRequest`] if multiple fields are missing IncompleteForm { missing_field: &'static str }, /// The request was invalid for some `reason` GenericBadRequest { reason: Cow<'static, str> }, /// Error interacting with the database DatabaseError { source: DatabaseError }, /// An error from wikarden WikiError { source: WikError }, /// failed authentication AuthenticationFailed { name: String }, /// A [`SessionID`] was invalid InvalidSession, /// Generic 403 Authorization Needed Generic403, /// Generic 404 Not Found Generic404, /// Some unknown error; 500 Internal Server Error Unknown, } impl RuntimeError { pub fn outside_root(request_path: String, system_path: Utf8PathBuf) -> Self { Self::InvalidPath { reason: InvalidPathReason::OutsideRoot, request_path, system_path, } } pub fn not_found(request_path: String, system_path: Utf8PathBuf) -> Self { Self::InvalidPath { reason: InvalidPathReason::NotFound, request_path, system_path, } } pub fn file_error(err: std::io::Error, system_path: Utf8PathBuf) -> Self { let reason = match err.kind() { ErrorKind::PermissionDenied => FileErrorReason::PermissionDenied, ErrorKind::NotFound => FileErrorReason::NotFound, _ => FileErrorReason::Unknown, }; Self::FileError { reason, source: err, system_path, } } pub fn not_a_template(system_path: Utf8PathBuf) -> Self { Self::TemplateError { reason: TemplateErrorReason::NotATemplate, source: None, system_path, } } pub fn template_parse_error(err: bempline::ParseError, system_path: Utf8PathBuf) -> Self { Self::TemplateError { reason: TemplateErrorReason::ParseError, source: Some(err), system_path, } } pub fn bad_request>>(reason: R) -> Self { Self::GenericBadRequest { reason: reason.into(), } } pub fn incomplete_form(field: &'static str) -> Self { Self::IncompleteForm { missing_field: field, } } pub fn authentication_failed(name: String) -> Self { Self::AuthenticationFailed { name } } pub fn generic_404() -> Self { RuntimeError::Generic404 } pub fn unknown() -> Self { RuntimeError::Unknown } pub fn status_code(&self) -> u16 { match self { Self::InvalidPath { .. } => 404, Self::FileError { reason, .. } => match reason { FileErrorReason::NotFound => 404, FileErrorReason::PermissionDenied => 404, FileErrorReason::Unknown => 500, }, Self::TemplateError { .. } => 500, Self::QueryParseError { .. } => 400, Self::GenericBadRequest { .. } => 400, Self::IncompleteForm { .. } => 400, Self::DatabaseError { .. } => 500, Self::WikiError { source } => match source { WikError::IllegalPath { .. } => 404, WikError::CanonicalizationFailed { .. } => 404, _ => 500, }, Self::AuthenticationFailed { .. } => 403, Self::InvalidSession => 403, Self::Generic403 => 403, Self::Generic404 => 404, Self::Unknown => 500, } } } impl fmt::Display for RuntimeError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { RuntimeError::InvalidPath { reason, request_path, system_path, } => write!( f, "invalid path. reason={reason} web={request_path}, system={system_path}" ), RuntimeError::FileError { source, system_path, .. } => { write!(f, "file error: {source}. system={system_path}") } RuntimeError::TemplateError { reason, source, system_path, } => match source { Some(pe) => write!(f, "template error: {pe}. system={system_path}"), None => write!(f, "template error. reason={reason}, system={system_path}"), }, RuntimeError::QueryParseError { source } => { write!(f, "failed to parse query. {source}") } RuntimeError::GenericBadRequest { reason } => { write!(f, "bad request {reason}") } RuntimeError::IncompleteForm { missing_field } => { write!(f, "missing field {missing_field} in form") } RuntimeError::DatabaseError { source } => { write!(f, "database error: {source}") } RuntimeError::WikiError { source } => { write!(f, "wiki error: {source}") } RuntimeError::AuthenticationFailed { name } => { write!(f, "authentication failed for '{name}'") } RuntimeError::InvalidSession => { write!(f, "the session id was invalid") } RuntimeError::Generic403 => write!(f, "generic 403"), RuntimeError::Generic404 => write!(f, "generic 404"), RuntimeError::Unknown => write!(f, "unknown error!"), } } } /// Errors users can see struct PresentableRuntimeError<'re>(&'re RuntimeError); impl<'re> fmt::Display for PresentableRuntimeError<'re> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { macro_rules! estr { (400) => { write!(f, "bad request") }; (400 incomplete $missing:ident) => { write!(f, "incomplete form. field {} is required", $missing) }; (403) => { write!(f, "authenticated failed") }; (404) => { write!(f, "the requested file could not be found") }; (404 $webpath:ident) => { write!(f, "file not found: '{}'", $webpath) }; (500) => { write!(f, "an unknown error occured!") }; } match &self.0 { RuntimeError::InvalidPath { request_path, .. } => { estr!(404 request_path) } RuntimeError::FileError { reason, .. } => match reason { FileErrorReason::NotFound | FileErrorReason::PermissionDenied => estr!(404), FileErrorReason::Unknown => estr!(500), }, RuntimeError::TemplateError { .. } => estr!(500), RuntimeError::QueryParseError { .. } => estr!(400), RuntimeError::GenericBadRequest { .. } => estr!(400), RuntimeError::IncompleteForm { missing_field } => estr!(400 incomplete missing_field), RuntimeError::DatabaseError { .. } => estr!(500), RuntimeError::WikiError { source } => match source { WikError::IllegalPath { wik, .. } => estr!(404 wik), WikError::CanonicalizationFailed { .. } => estr!(404), _ => estr!(500), }, RuntimeError::AuthenticationFailed { .. } => estr!(403), RuntimeError::InvalidSession => estr!(403), RuntimeError::Generic403 => estr!(403), RuntimeError::Generic404 => estr!(404), RuntimeError::Unknown => estr!(500), } } } impl IntoResponse for RuntimeError { fn into_response(self) -> Response { let status = self.status_code(); let presentable = PresentableRuntimeError(&self); let mut response = make_basic_error(status, presentable); response.extensions_mut().insert(Arc::new(self)); response } } fn make_basic_error(status: u16, msg: S) -> Response { let error_str = format!( "\n\ \t{status}\n\ \t\n\ \t\t

{status}

\n\ \t\t
\n\ \t\t

{msg}

\n\ \t\n\ " ); Response::builder() .status(status) .header("content-type", "text/html") .body(Body::from(error_str)) .unwrap() .into_response() } impl From for RuntimeError { fn from(qpe: QueryParseError) -> Self { RuntimeError::QueryParseError { source: qpe } } } impl From for RuntimeError { fn from(dbe: DatabaseError) -> Self { RuntimeError::DatabaseError { source: dbe } } } impl From for RuntimeError { fn from(we: WikError) -> Self { RuntimeError::WikiError { source: we } } }