-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
The error page is displayed when a handler returns a Result::Err. A nice-to-have thing would be to catch panics as well.
- Loading branch information
Showing
6 changed files
with
357 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
use std::sync::Arc; | ||
|
||
use askama::Template; | ||
use log::error; | ||
|
||
use crate::router::Router; | ||
use crate::{Error, Result, StatusCode}; | ||
|
||
#[derive(Debug)] | ||
pub(super) struct FlareonDiagnostics { | ||
router: Arc<Router>, | ||
} | ||
|
||
impl FlareonDiagnostics { | ||
#[must_use] | ||
pub(super) fn new(router: Arc<Router>) -> Self { | ||
Self { router } | ||
} | ||
} | ||
|
||
#[derive(Debug, Template)] | ||
#[template(path = "error.html")] | ||
struct ErrorPageTemplate { | ||
error_data: Vec<ErrorData>, | ||
route_data: Vec<RouteData>, | ||
} | ||
|
||
#[derive(Debug)] | ||
struct ErrorData { | ||
description: String, | ||
debug_str: String, | ||
is_flareon_error: bool, | ||
} | ||
|
||
#[derive(Debug)] | ||
struct RouteData { | ||
index: String, | ||
path: String, | ||
kind: String, | ||
name: String, | ||
} | ||
|
||
#[must_use] | ||
pub(super) fn handle_response_error( | ||
error: Error, | ||
diagnostics: FlareonDiagnostics, | ||
) -> axum::response::Response { | ||
let response = build_error_response(error, diagnostics); | ||
|
||
match response { | ||
Ok(error_str) => axum::response::Response::builder() | ||
.status(StatusCode::INTERNAL_SERVER_ERROR) | ||
.body(axum::body::Body::new(error_str)) | ||
.unwrap_or_else(|_| build_flareon_failure_page()), | ||
Err(error) => { | ||
error!("Failed to render error page: {}", error); | ||
build_flareon_failure_page() | ||
} | ||
} | ||
} | ||
|
||
fn build_error_response(error: Error, diagnostics: FlareonDiagnostics) -> Result<String> { | ||
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<RouteData>, | ||
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_error_data(vec: &mut Vec<ErrorData>, error: &(dyn std::error::Error + 'static)) { | ||
let data = ErrorData { | ||
description: error.to_string(), | ||
debug_str: format!("{error:#?}"), | ||
is_flareon_error: error.is::<Error>(), | ||
}; | ||
vec.push(data); | ||
|
||
if let Some(source) = error.source() { | ||
build_error_data(vec, source); | ||
} | ||
} | ||
|
||
const FAILURE_PAGE: &[u8] = include_bytes!("../templates/fail.html"); | ||
|
||
/// A last-resort error page. | ||
/// | ||
/// This page is displayed when an error occurs that prevents Flareon from | ||
/// rendering a proper error page. This page is very simple and should only be | ||
/// displayed in the event of a catastrophic failure, likely caused by a bug in | ||
/// Flareon itself. | ||
#[must_use] | ||
fn build_flareon_failure_page() -> axum::response::Response { | ||
axum::response::Response::builder() | ||
.status(StatusCode::INTERNAL_SERVER_ERROR) | ||
.body(axum::body::Body::from(FAILURE_PAGE)) | ||
.expect("Building the Flareon failure page should not fail") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
*, *::before, *::after { | ||
box-sizing: border-box; | ||
} | ||
|
||
* { | ||
margin: 0; | ||
padding: 0; | ||
border: 0; | ||
font-size: 100%; | ||
font: inherit; | ||
font-family: "Open Sans", sans-serif; | ||
vertical-align: baseline; | ||
} | ||
|
||
body { | ||
line-height: 1.5; | ||
} | ||
|
||
img, picture, video, canvas, svg { | ||
display: block; | ||
max-width: 100%; | ||
} | ||
|
||
input, button, textarea, select { | ||
font: inherit; | ||
} | ||
|
||
p, h1, h2, h3, h4, h5, h6 { | ||
overflow-wrap: break-word; | ||
} | ||
|
||
em { | ||
font-style: italic; | ||
} | ||
|
||
body { | ||
background-color: #e8edf1; | ||
padding: 1rem; | ||
} | ||
|
||
h1 { | ||
margin: -1rem -1rem 1rem -1rem; | ||
padding: 0.5rem; | ||
font-size: 2rem; | ||
font-weight: lighter; | ||
|
||
background-color: rgb(255, 63, 79); | ||
color: white; | ||
} | ||
|
||
h2 { | ||
font-size: 1.5rem; | ||
margin-top: .75rem; | ||
} | ||
|
||
h3 { | ||
font-size: 1.25rem; | ||
margin-top: .5rem; | ||
} | ||
|
||
table, th, td { | ||
border: 1px solid #d9dde1; | ||
border-collapse: collapse; | ||
} | ||
|
||
table > thead { | ||
vertical-align: bottom; | ||
} | ||
|
||
table th { | ||
font-weight: bold; | ||
} | ||
|
||
th, td { | ||
padding: .5rem; | ||
border-bottom-color: #d9dde1; | ||
border-bottom-width: 1px; | ||
} | ||
|
||
pre { | ||
white-space: pre-wrap; | ||
word-wrap: break-word; | ||
|
||
font-size: .75rem; | ||
font-family: "Noto Sans Mono", monospace; | ||
} | ||
|
||
.badge { | ||
color: #fff; | ||
background-color: #1876ff; | ||
|
||
display: inline-block; | ||
font-size: 0.7em; | ||
padding: 0.25em 0.5em; | ||
border-radius: 1em; | ||
line-height: 1; | ||
|
||
text-align: center; | ||
white-space: nowrap; | ||
vertical-align: baseline; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
<!DOCTYPE html> | ||
<html lang="en"> | ||
<head> | ||
<meta charset="UTF-8"> | ||
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||
<title>Flareon failure</title> | ||
|
||
<style> | ||
{% include "error.css" %} | ||
</style> | ||
</head> | ||
<body> | ||
<h1>Flareon failure</h1> | ||
<p>An error occurred when handling a request.</p> | ||
|
||
<h2>Errors</h2> | ||
|
||
<table> | ||
<thead> | ||
<tr> | ||
<th scope="col">#</th> | ||
<th scope="col">Description</th> | ||
<th scope="col">Structure</th> | ||
</tr> | ||
</thead> | ||
<tbody> | ||
{% for error in error_data %} | ||
<tr> | ||
<th scope="row">{{ loop.index0 }}</th> | ||
<td>{{ error.description }}</td> | ||
<td> | ||
{% if error.is_flareon_error %}<code class="badge">flareon::Error</code>{% endif %} | ||
<pre>{{ error.debug_str }}</pre> | ||
</td> | ||
</tr> | ||
{% endfor %} | ||
</tbody> | ||
</table> | ||
|
||
<h2>Diagnostics</h2> | ||
|
||
<h3>Routes</h3> | ||
|
||
<table> | ||
<thead> | ||
<tr> | ||
<th scope="col">#</th> | ||
<th scope="col">URL</th> | ||
<th scope="col">Type</th> | ||
<th scope="col">Name</th> | ||
</tr> | ||
</thead> | ||
<tbody> | ||
{% for route in route_data %} | ||
<tr> | ||
<th scope="row">{{ route.index }}</th> | ||
<td>{% if route.path.is_empty() %}<em><empty></em>{% else %}{{ route.path }}{% endif %}</td> | ||
<td>{{ route.kind }}</td> | ||
<td>{% if route.name.is_empty() %}<em><none></em>{% else %}{{ route.name }}{% endif %}</td> | ||
</tr> | ||
{% endfor %} | ||
</tbody> | ||
</table> | ||
|
||
</body> | ||
</html> |
Oops, something went wrong.