From 054ccec1df01c8e943f304d0b159ccb6eb4b840c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Ma=C4=87kowski?= Date: Sat, 28 Sep 2024 00:40:06 +0100 Subject: [PATCH] feat: catch panics; more info on the error page (#35) --- Cargo.lock | 32 +-- Cargo.toml | 2 + flareon/Cargo.toml | 2 + flareon/src/config.rs | 16 ++ flareon/src/error_page.rs | 400 +++++++++++++++++++++++++++++------ flareon/src/lib.rs | 78 +++++-- flareon/src/router.rs | 7 +- flareon/templates/error.css | 73 ++++++- flareon/templates/error.html | 80 ++++++- 9 files changed, 583 insertions(+), 107 deletions(-) create mode 100644 flareon/src/config.rs diff --git a/Cargo.lock b/Cargo.lock index e7be322..d9fb93e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,18 +4,18 @@ version = 3 [[package]] name = "addr2line" -version = "0.22.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +checksum = "f5fb1d8e4442bd405fdfd1dacb42792696b0cf9cb15882e5d097b742a676d375" dependencies = [ "gimli", ] [[package]] -name = "adler" -version = "1.0.2" +name = "adler2" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" [[package]] name = "ahash" @@ -263,17 +263,17 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.73" +version = "0.3.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" dependencies = [ "addr2line", - "cc", "cfg-if", "libc", "miniz_oxide", "object", "rustc-demangle", + "windows-targets 0.52.6", ] [[package]] @@ -796,6 +796,7 @@ dependencies = [ "async-stream", "async-trait", "axum", + "backtrace", "bytes", "chrono", "derive_builder", @@ -805,6 +806,7 @@ dependencies = [ "form_urlencoded", "futures", "futures-core", + "futures-util", "http", "http-body", "http-body-util", @@ -1025,9 +1027,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.29.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" +checksum = "32085ea23f3234fc7846555e85283ba4de91e21016dc0455a16286d87a292d64" [[package]] name = "glob" @@ -1292,9 +1294,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.155" +version = "0.2.159" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" [[package]] name = "libm" @@ -1382,11 +1384,11 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.4" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" dependencies = [ - "adler", + "adler2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 9132197..09224b4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ askama = "0.12.1" async-stream = "0.3.5" async-trait = "0.1.80" axum = "0.7.5" +backtrace = "0.3.74" bytes = "1.7.1" cargo_toml = "0.20.4" chrono = { version = "0.4.38", features = ["serde"] } @@ -40,6 +41,7 @@ flareon_macros = { path = "flareon-macros" } form_urlencoded = "1.2.1" futures = "0.3.30" futures-core = "0.3.30" +futures-util = "0.3.30" glob = "0.3.1" http = "1.1.0" http-body = "1.0.1" diff --git a/flareon/Cargo.toml b/flareon/Cargo.toml index 0044cad..fa16180 100644 --- a/flareon/Cargo.toml +++ b/flareon/Cargo.toml @@ -9,6 +9,7 @@ description = "Modern web framework focused on speed and ease of use." askama.workspace = true async-trait.workspace = true axum.workspace = true +backtrace.workspace = true bytes.workspace = true chrono.workspace = true derive_builder.workspace = true @@ -16,6 +17,7 @@ derive_more.workspace = true flareon_macros.workspace = true form_urlencoded.workspace = true futures-core.workspace = true +futures-util.workspace = true http.workspace = true http-body.workspace = true http-body-util.workspace = true diff --git a/flareon/src/config.rs b/flareon/src/config.rs new file mode 100644 index 0000000..c7e436a --- /dev/null +++ b/flareon/src/config.rs @@ -0,0 +1,16 @@ +/// Debug mode flag +/// +/// This enables some expensive operations that are useful for debugging, such +/// as logging additional information, and collecting some extra diagnostics +/// for generating error pages. This hurts the performance, so it should be +/// disabled for production. +/// +/// This is `true` when the application is compiled in debug mode, and `false` +/// when it is compiled in release mode. +pub(crate) const DEBUG_MODE: bool = cfg!(debug_assertions); + +/// Whether to display a nice, verbose error page when an error, panic, or +/// 404 "Not Found" occurs. +pub(crate) const DISPLAY_ERROR_PAGE: bool = DEBUG_MODE; + +pub(crate) const REGISTER_PANIC_HOOK: bool = true; diff --git a/flareon/src/error_page.rs b/flareon/src/error_page.rs index 5dc272d..d2859c0 100644 --- a/flareon/src/error_page.rs +++ b/flareon/src/error_page.rs @@ -1,3 +1,5 @@ +use std::any::Any; +use std::panic::PanicHookInfo; use std::sync::Arc; use askama::Template; @@ -6,33 +8,202 @@ use log::error; use crate::router::Router; use crate::{Error, Result, StatusCode}; +/// Added as a Response extension to trigger displaying the error page. +/// +/// If the global request handler finds this in a response generated by a router +/// or view, it intercepts the response (given that +/// [`crate::config::DISPLAY_ERROR_PAGE`] is `true`) and displays the error page +/// instead. +#[derive(Debug, Copy, Clone)] +pub(crate) enum ErrorPageTrigger { + NotFound, +} + #[derive(Debug)] pub(super) struct FlareonDiagnostics { router: Arc, + request_parts: Option, } impl FlareonDiagnostics { #[must_use] - pub(super) fn new(router: Arc) -> Self { - Self { router } + pub(super) fn new(router: Arc, request_parts: Option) -> Self { + Self { + router, + request_parts, + } } } #[derive(Debug, Template)] #[template(path = "error.html")] struct ErrorPageTemplate { + kind: Kind, + panic_string: Option, + panic_location: Option, + backtrace: Option, error_data: Vec, route_data: Vec, + request_data: Option, } -#[derive(Debug)] +#[derive(Debug, Default, Clone)] +struct ErrorPageTemplateBuilder { + kind: Kind, + panic_string: Option, + panic_location: Option, + backtrace: Option, + error_data: Vec, + route_data: Vec, + request_data: Option, +} + +impl ErrorPageTemplateBuilder { + #[must_use] + fn not_found() -> Self { + Self { + kind: Kind::NotFound, + ..Default::default() + } + } + + #[must_use] + fn error(error: Error) -> Self { + let mut error_data = Vec::new(); + Self::build_error_data(&mut error_data, &error); + + Self { + kind: Kind::Error, + error_data, + ..Default::default() + } + } + + #[must_use] + fn panic(panic_payload: Box) -> Self { + Self { + kind: Kind::Panic, + panic_string: Self::get_panic_string(panic_payload), + panic_location: PANIC_LOCATION.take(), + backtrace: PANIC_BACKTRACE.take(), + ..Default::default() + } + } + + fn diagnostics(&mut self, diagnostics: FlareonDiagnostics) -> &mut Self { + self.route_data.clear(); + Self::build_route_data(&mut self.route_data, &diagnostics.router, "", ""); + self.request_data = diagnostics + .request_parts + .as_ref() + .map(Self::build_request_data); + self + } + + fn build_route_data( + route_data: &mut Vec, + router: &Router, + url_prefix: &str, + index_prefix: &str, + ) { + for (index, route) in router.routes().iter().enumerate() { + route_data.push(RouteData { + index: format!("{index_prefix}{index}"), + path: route.url(), + kind: match route.kind() { + crate::router::RouteKind::Router => if route_data.is_empty() { + "Root Router" + } else { + "Router" + } + .to_owned(), + crate::router::RouteKind::Handler => "View".to_owned(), + }, + name: route.name().unwrap_or_default().to_owned(), + }); + + if let Some(inner_router) = route.router() { + Self::build_route_data( + route_data, + inner_router, + &format!("{}{}", url_prefix, route.url()), + &format!("{index_prefix}{index}."), + ); + } + } + } + + fn build_error_data(vec: &mut Vec, error: &(dyn std::error::Error + 'static)) { + let data = ErrorData { + description: error.to_string(), + debug_str: format!("{error:#?}"), + is_flareon_error: error.is::(), + }; + vec.push(data); + + if let Some(source) = error.source() { + Self::build_error_data(vec, source); + } + } + + #[must_use] + fn build_request_data(parts: &http::request::Parts) -> RequestData { + RequestData { + method: parts.method.to_string(), + url: parts.uri.to_string(), + protocol_version: format!("{:?}", parts.version), + headers: parts + .headers + .iter() + .map(|(name, value)| { + ( + name.as_str().to_owned(), + String::from_utf8_lossy(value.as_ref()).into_owned(), + ) + }) + .collect(), + } + } + + #[must_use] + fn get_panic_string(panic_payload: Box) -> Option { + if let Some(&panic_string) = panic_payload.downcast_ref::<&str>() { + Some(panic_string.to_owned()) + } else { + panic_payload.downcast_ref::().cloned() + } + } + + fn render(&self) -> askama::Result { + ErrorPageTemplate { + kind: self.kind, + panic_string: self.panic_string.clone(), + panic_location: self.panic_location.clone(), + backtrace: self.backtrace.clone(), + error_data: self.error_data.clone(), + route_data: self.route_data.clone(), + request_data: self.request_data.clone(), + } + .render() + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)] +enum Kind { + NotFound, + #[default] + Error, + Panic, +} + +#[derive(Debug, Clone)] struct ErrorData { description: String, debug_str: String, is_flareon_error: bool, } -#[derive(Debug)] +#[derive(Debug, Clone)] struct RouteData { index: String, path: String, @@ -40,16 +211,49 @@ struct RouteData { name: String, } +#[derive(Debug, Clone)] +struct RequestData { + method: String, + url: String, + protocol_version: String, + headers: Vec<(String, String)>, +} + +#[must_use] +pub(super) fn handle_not_found(diagnostics: FlareonDiagnostics) -> axum::response::Response { + build_response(build_not_found_response(diagnostics), StatusCode::NOT_FOUND) +} + +#[must_use] +pub(super) fn handle_response_panic( + panic_payload: Box, + diagnostics: FlareonDiagnostics, +) -> axum::response::Response { + build_response( + build_panic_response(panic_payload, diagnostics), + StatusCode::INTERNAL_SERVER_ERROR, + ) +} + #[must_use] pub(super) fn handle_response_error( error: Error, diagnostics: FlareonDiagnostics, ) -> axum::response::Response { - let response = build_error_response(error, diagnostics); + build_response( + build_error_response(error, diagnostics), + StatusCode::INTERNAL_SERVER_ERROR, + ) +} - match response { +#[must_use] +fn build_response( + response_string: Result, + status_code: StatusCode, +) -> axum::response::Response { + match response_string { Ok(error_str) => axum::response::Response::builder() - .status(StatusCode::INTERNAL_SERVER_ERROR) + .status(status_code) .body(axum::body::Body::new(error_str)) .unwrap_or_else(|_| build_flareon_failure_page()), Err(error) => { @@ -59,63 +263,25 @@ pub(super) fn handle_response_error( } } -fn build_error_response(error: Error, diagnostics: FlareonDiagnostics) -> Result { - let mut error_data = Vec::new(); - build_error_data(&mut error_data, &error); - let mut route_data = Vec::new(); - build_route_data(&mut route_data, &diagnostics.router, "", ""); - - let template = ErrorPageTemplate { - error_data, - route_data, - }; - Ok(template.render()?) -} - -fn build_route_data( - route_data: &mut Vec, - router: &Router, - url_prefix: &str, - index_prefix: &str, -) { - for (index, route) in router.routes().iter().enumerate() { - route_data.push(RouteData { - index: format!("{index_prefix}{index}"), - path: route.url(), - kind: match route.kind() { - crate::router::RouteKind::Router => if route_data.is_empty() { - "RootRouter" - } else { - "Router" - } - .to_owned(), - crate::router::RouteKind::Handler => "View".to_owned(), - }, - name: route.name().unwrap_or_default().to_owned(), - }); - - if let Some(inner_router) = route.router() { - build_route_data( - route_data, - inner_router, - &format!("{}{}", url_prefix, route.url()), - &format!("{index_prefix}{index}."), - ); - } - } +fn build_not_found_response(diagnostics: FlareonDiagnostics) -> Result { + Ok(ErrorPageTemplateBuilder::not_found() + .diagnostics(diagnostics) + .render()?) } -fn build_error_data(vec: &mut Vec, error: &(dyn std::error::Error + 'static)) { - let data = ErrorData { - description: error.to_string(), - debug_str: format!("{error:#?}"), - is_flareon_error: error.is::(), - }; - vec.push(data); +fn build_panic_response( + panic_payload: Box, + diagnostics: FlareonDiagnostics, +) -> Result { + Ok(ErrorPageTemplateBuilder::panic(panic_payload) + .diagnostics(diagnostics) + .render()?) +} - if let Some(source) = error.source() { - build_error_data(vec, source); - } +fn build_error_response(error: Error, diagnostics: FlareonDiagnostics) -> Result { + Ok(ErrorPageTemplateBuilder::error(error) + .diagnostics(diagnostics) + .render()?) } const FAILURE_PAGE: &[u8] = include_bytes!("../templates/fail.html"); @@ -133,3 +299,115 @@ fn build_flareon_failure_page() -> axum::response::Response { .body(axum::body::Body::from(FAILURE_PAGE)) .expect("Building the Flareon failure page should not fail") } + +thread_local! { + static PANIC_LOCATION: std::cell::RefCell> = const { std::cell::RefCell::new(None) }; + static PANIC_BACKTRACE: std::cell::RefCell> = const { std::cell::RefCell::new(None) }; +} + +pub(super) fn error_page_panic_hook(info: &PanicHookInfo<'_>) { + let location = info.location().map(|location| format!("{location}")); + PANIC_LOCATION.replace(location); + + PANIC_BACKTRACE.replace(Some(__flareon_create_backtrace())); +} + +// inline(never) is added to make sure there is a separate frame for this +// function so that it can be used to find the start of the backtrace. +#[inline(never)] +fn __flareon_create_backtrace() -> Backtrace { + let mut backtrace = Vec::new(); + let mut start = false; + backtrace::trace(|frame| { + let frame = StackFrame::from(frame); + if start { + backtrace.push(frame); + } else if frame.symbol_name().contains("__flareon_create_backtrace") { + start = true; + } + + true + }); + + Backtrace { frames: backtrace } +} + +#[derive(Debug, Clone)] +struct Backtrace { + frames: Vec, +} + +impl Backtrace { + #[must_use] + fn frames(&self) -> &[StackFrame] { + &self.frames + } +} + +#[derive(Debug, Clone)] +struct StackFrame { + symbol_name: Option, + filename: Option, + lineno: Option, + colno: Option, +} + +impl StackFrame { + #[must_use] + fn symbol_name(&self) -> String { + self.symbol_name + .as_deref() + .unwrap_or("") + .to_string() + } + + #[must_use] + fn location(&self) -> String { + if let Some(filename) = self.filename.as_deref() { + let mut s = filename.to_owned(); + + if let Some(line_no) = self.lineno { + s = format!("{s}:{line_no}"); + + if let Some(col_no) = self.colno { + s = format!("{s}:{col_no}"); + } + } + + s + } else { + "".to_string() + } + } +} + +impl From<&backtrace::Frame> for StackFrame { + fn from(frame: &backtrace::Frame) -> Self { + let mut symbol_name = None; + let mut filename = None; + let mut lineno = None; + let mut colno = None; + + backtrace::resolve_frame(frame, |symbol| { + if let Some(name) = symbol.name() { + symbol_name = Some(name.to_string()); + } + if let Some(file) = symbol.filename() { + filename = Some(file.display().to_string()); + } + if let Some(line) = symbol.lineno() { + lineno = Some(line); + } + if let Some(col) = symbol.colno() { + colno = Some(col); + } + }); + + Self { + symbol_name, + filename, + lineno, + colno, + } + } +} diff --git a/flareon/src/lib.rs b/flareon/src/lib.rs index 4a3ce13..16649b9 100644 --- a/flareon/src/lib.rs +++ b/flareon/src/lib.rs @@ -20,6 +20,7 @@ mod headers; #[doc(hidden)] #[path = "private.rs"] pub mod __private; +mod config; mod error_page; pub mod middleware; pub mod request; @@ -29,6 +30,7 @@ pub mod test; use std::fmt::{Debug, Formatter}; use std::future::{poll_fn, Future}; +use std::panic::AssertUnwindSafe; use std::pin::Pin; use std::sync::Arc; use std::task::{Context, Poll}; @@ -41,13 +43,15 @@ use derive_more::{Deref, From}; pub use error::Error; use flareon::router::RouterService; use futures_core::Stream; +use futures_util::FutureExt; +use http::request::Parts; use http_body::{Frame, SizeHint}; use log::info; use request::Request; use router::{Route, Router}; use tower::Service; -use crate::error_page::FlareonDiagnostics; +use crate::error_page::{ErrorPageTrigger, FlareonDiagnostics}; use crate::response::Response; /// A type alias for a result that can return a `flareon::Error`. @@ -449,13 +453,44 @@ where let handler = |axum_request: axum::extract::Request| async move { let request = request_axum_to_flareon(axum_request, router.clone()); - - pass_to_axum(request, &mut handler) - .await - .unwrap_or_else(|error| { - let diagnostics = FlareonDiagnostics::new(router.clone()); - error_page::handle_response_error(error, diagnostics) - }) + let (request_parts, request) = request_parts_for_diagnostics(request); + + let catch_unwind_response = AssertUnwindSafe(pass_to_axum(request, &mut handler)) + .catch_unwind() + .await; + + let show_error_page = match &catch_unwind_response { + Ok(response) => match response { + Ok(response) => response.extensions().get::().is_some(), + Err(_) => true, + }, + Err(_) => { + // handler panicked + true + } + }; + + if show_error_page { + let diagnostics = FlareonDiagnostics::new(router.clone(), request_parts); + + match catch_unwind_response { + Ok(response) => match response { + Ok(response) => match response + .extensions() + .get::() + .expect("ErrorPageTrigger already has been checked to be Some") + { + ErrorPageTrigger::NotFound => error_page::handle_not_found(diagnostics), + }, + Err(error) => error_page::handle_response_error(error, diagnostics), + }, + Err(error) => error_page::handle_response_panic(error, diagnostics), + } + } else { + catch_unwind_response + .expect("Error page should be shown if the response is not a panic") + .expect("Error page should be shown if the response is not an error") + } }; eprintln!( @@ -464,18 +499,33 @@ where .local_addr() .map_err(|e| Error::StartServer { source: e })? ); + if config::REGISTER_PANIC_HOOK { + std::panic::set_hook(Box::new(error_page::error_page_panic_hook)); + } axum::serve(listener, handler.into_make_service()) .await .map_err(|e| Error::StartServer { source: e })?; + if config::REGISTER_PANIC_HOOK { + let _ = std::panic::take_hook(); + } Ok(()) } +fn request_parts_for_diagnostics(request: Request) -> (Option, Request) { + if config::DEBUG_MODE { + let (parts, body) = request.into_parts(); + let parts_clone = parts.clone(); + let request = Request::from_parts(parts, body); + (Some(parts_clone), request) + } else { + (None, request) + } +} + fn request_axum_to_flareon(axum_request: axum::extract::Request, router: Arc) -> Request { - let (parts, body) = axum_request.into_parts(); - let mut request = Request::from_parts(parts, Body::axum(body)); + let mut request = axum_request.map(Body::axum); prepare_request(&mut request, router); - request } @@ -491,11 +541,7 @@ where poll_fn(|cx| handler.poll_ready(cx)).await?; let response = handler.call(request).await?; - let (parts, body) = response.into_parts(); - Ok(http::Response::from_parts( - parts, - axum::body::Body::new(body), - )) + Ok(response.map(axum::body::Body::new)) } /// A trait for types that can be used to render them as HTML. diff --git a/flareon/src/router.rs b/flareon/src/router.rs index 52a4e32..673f8ce 100644 --- a/flareon/src/router.rs +++ b/flareon/src/router.rs @@ -9,6 +9,7 @@ use axum::http::StatusCode; use bytes::Bytes; use log::debug; +use crate::error_page::ErrorPageTrigger; use crate::request::{Request, RequestExt}; use crate::response::{Response, ResponseExt}; use crate::router::path::{PathMatcher, ReverseParamMap}; @@ -227,10 +228,12 @@ impl Route { } fn handle_not_found() -> Response { - Response::new_html( + let mut response = Response::new_html( StatusCode::NOT_FOUND, Body::fixed(Bytes::from("404 Not Found")), - ) + ); + response.extensions_mut().insert(ErrorPageTrigger::NotFound); + response } #[derive(Debug, Copy, Clone, PartialEq, Eq)] diff --git a/flareon/templates/error.css b/flareon/templates/error.css index 789971d..35ce866 100644 --- a/flareon/templates/error.css +++ b/flareon/templates/error.css @@ -8,12 +8,13 @@ border: 0; font-size: 100%; font: inherit; - font-family: "Open Sans", sans-serif; vertical-align: baseline; } body { line-height: 1.5; + font-family: "Open Sans", sans-serif; + font-size: small; } img, picture, video, canvas, svg { @@ -34,7 +35,7 @@ em { } body { - background-color: #e8edf1; + background-color: #f6f6f6; padding: 1rem; } @@ -43,9 +44,16 @@ h1 { padding: 0.5rem; font-size: 2rem; font-weight: lighter; +} - background-color: rgb(255, 63, 79); - color: white; +h1.error { + background-color: #ff3f4fff; + color: #fff; +} + +h1.warning { + background-color: #ffdb5b; + color: #000; } h2 { @@ -54,8 +62,15 @@ h2 { } h3 { - font-size: 1.25rem; + font-size: 1.1rem; margin-top: .5rem; + font-weight: bold; +} + +table { + font-size: .9rem; + background-color: #fff; + } table, th, td { @@ -63,6 +78,10 @@ table, th, td { border-collapse: collapse; } +table.compact { + font-size: 0.75rem; +} + table > thead { vertical-align: bottom; } @@ -71,17 +90,32 @@ table th { font-weight: bold; } -th, td { - padding: .5rem; +table th.index { + text-align: right; +} + +table th, td { + padding: .4rem; border-bottom-color: #d9dde1; border-bottom-width: 1px; } +table.compact th, td { + padding: .1rem .25rem; +} + pre { white-space: pre-wrap; word-wrap: break-word; + font-family: "Noto Sans Mono", monospace; +} + +pre.small { font-size: .75rem; +} + +samp { font-family: "Noto Sans Mono", monospace; } @@ -99,3 +133,28 @@ pre { white-space: nowrap; vertical-align: baseline; } + +.backtrace-table .frame .symbol-name { + font-weight: bold; +} + +.backtrace-table .frame .symbol-location { + padding-left: 4em; +} + +.backtrace-table { + display: table; +} + +.backtrace-table .backtrace-row { + display: table-row; +} + +.backtrace-table .backtrace-cell { + display: table-cell; +} + +.backtrace-table .frame-index { + text-align: right; + padding-right: .5em; +} diff --git a/flareon/templates/error.html b/flareon/templates/error.html index 1f7fb76..ba3f7cb 100644 --- a/flareon/templates/error.html +++ b/flareon/templates/error.html @@ -3,6 +3,7 @@ + Flareon failure -

Flareon failure

-

An error occurred when handling a request.

+

{% match kind %}{% when Kind::NotFound %}Not found{% else %}Flareon failure{% endmatch %}

+

{% match kind %}{% when Kind::NotFound %}The URL requested could not be found.{% when Kind::Panic %}The request handler has panicked.{% else %}An error occurred while handling a request.{% endmatch %}

-

Errors

+{% match kind %} + {% when Kind::Panic %} +

Panic data

+ +{% match panic_string %}{% when Some with (panic_string) %}
{{ panic_string }}
{% else %}Panic payload unavailable or not a string.{% endmatch %} +{% match panic_location %}{% when Some with (panic_location) %}

at {{ panic_location }}

{% else %}{% endmatch %} + {% else %} +{% endmatch %} + +{% if kind == Kind::Error || kind == Kind::Panic -%} +

Backtrace

+ + {% match backtrace -%} + {% when Some with (backtrace) %} +
+ {% for frame in backtrace.frames() %} +
+
{{loop.index0}}:
+
+
{{ frame.symbol_name() }}
+
at {{ frame.location() }}
+
+
+ {% endfor %} +
+ {% else -%} +

Backtrace unavailable.

+ {%- endmatch %} +{% endif %} + +{% if !error_data.is_empty() -%} +

Error chain

@@ -26,16 +58,17 @@

Errors

{% for error in error_data %} - + {% endfor %}
{{ loop.index0 }}{{ loop.index0 }} {{ error.description }} {% if error.is_flareon_error %}flareon::Error{% endif %} -
{{ error.debug_str }}
+
{{ error.debug_str }}
+{%- endif %}

Diagnostics

@@ -53,7 +86,7 @@

Routes

{% for route in route_data %} - {{ route.index }} + {{ route.index }} {% if route.path.is_empty() %}<empty>{% else %}{{ route.path }}{% endif %} {{ route.kind }} {% if route.name.is_empty() %}<none>{% else %}{{ route.name }}{% endif %} @@ -62,5 +95,40 @@

Routes

+{% match request_data -%} +{% when Some with (request_data) -%} +

Request

+ +

Method

+

{{ request_data.method }}

+ +

URL

+

{{ request_data.url }}

+ +

Protocol version

+

{{ request_data.protocol_version }}

+ +

Headers

+ + + + + + + + + + {% for (header, value) in request_data.headers %} + + + + + {% endfor %} + +
HeaderValue
{{ header }}{{ value }}
+ +{% when None -%} +{%- endmatch %} +