Skip to content

Commit 7ea3ec0

Browse files
committed
feat: 🥅 set up proper error handling
1 parent 5b403b2 commit 7ea3ec0

File tree

7 files changed

+244
-69
lines changed

7 files changed

+244
-69
lines changed

examples/showcase/app/src/lib.rs

+30-8
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ pub mod pages;
33
use sycamore::prelude::*;
44
use sycamore_router::{Route, BrowserRouter};
55
use wasm_bindgen::prelude::*;
6-
use perseus::shell::app_shell;
6+
use perseus::shell::{app_shell, ErrorPages};
77

88
// Define our routes
99
#[derive(Route)]
@@ -30,6 +30,20 @@ enum AppRoute {
3030
NotFound
3131
}
3232

33+
fn get_error_pages() -> ErrorPages {
34+
let mut error_pages = ErrorPages::new(Box::new(|_, _, _| template! {
35+
p { "Another error occurred." }
36+
}));
37+
error_pages.add_page(404, Box::new(|_, _, _| template! {
38+
p { "Page not found." }
39+
}));
40+
error_pages.add_page(400, Box::new(|_, _, _| template! {
41+
p { "Client error occurred..." }
42+
}));
43+
44+
error_pages
45+
}
46+
3347
// This is deliberately purely client-side rendered
3448
#[wasm_bindgen]
3549
pub fn run() -> Result<(), JsValue> {
@@ -47,34 +61,42 @@ pub fn run() -> Result<(), JsValue> {
4761
||
4862
template! {
4963
BrowserRouter(|route: AppRoute| {
64+
// TODO improve performance rather than naively copying error pages for every template
5065
match route {
5166
AppRoute::Index => app_shell(
5267
"index".to_string(),
53-
pages::index::template_fn()
68+
pages::index::template_fn(),
69+
get_error_pages()
5470
),
5571
AppRoute::About => app_shell(
5672
"about".to_string(),
57-
pages::about::template_fn()
73+
pages::about::template_fn(),
74+
get_error_pages()
5875
),
5976
AppRoute::Post { slug } => app_shell(
6077
format!("post/{}", slug),
61-
pages::post::template_fn()
78+
pages::post::template_fn(),
79+
get_error_pages()
6280
),
6381
AppRoute::NewPost => app_shell(
6482
"post/new".to_string(),
65-
pages::new_post::template_fn()
83+
pages::new_post::template_fn(),
84+
get_error_pages()
6685
),
6786
AppRoute::Ip => app_shell(
6887
"ip".to_string(),
69-
pages::ip::template_fn()
88+
pages::ip::template_fn(),
89+
get_error_pages()
7090
),
7191
AppRoute::Time { slug } => app_shell(
7292
format!("timeisr/{}", slug),
73-
pages::time::template_fn()
93+
pages::time::template_fn(),
94+
get_error_pages()
7495
),
7596
AppRoute::TimeRoot => app_shell(
7697
"time".to_string(),
77-
pages::time_root::template_fn()
98+
pages::time_root::template_fn(),
99+
get_error_pages()
78100
),
79101
AppRoute::NotFound => template! {
80102
p {"Not Found."}

examples/showcase/app/src/pages/ip.rs

+3-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
use serde::{Serialize, Deserialize};
44
use sycamore::prelude::{template, component, GenericNode, Template as SycamoreTemplate};
55
use perseus::template::Template;
6+
use perseus::errors::ErrorCause;
67

78
#[derive(Serialize, Deserialize)]
89
pub struct IpPageProps {
@@ -26,7 +27,8 @@ pub fn get_page<G: GenericNode>() -> Template<G> {
2627
.template(template_fn())
2728
}
2829

29-
pub async fn get_request_state(_path: String) -> Result<String, String> {
30+
pub async fn get_request_state(_path: String) -> Result<String, (String, ErrorCause)> {
31+
// Err(("this is a test error!".to_string(), ErrorCause::Client(None)))
3032
Ok(serde_json::to_string(
3133
&IpPageProps {
3234
ip: "x.x.x.x".to_string()

examples/showcase/app/src/pages/post.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ pub async fn get_static_props(path: String) -> Result<String, String> {
4444
}
4545
).unwrap())
4646
}
47-
// TODO
47+
4848
pub async fn get_static_paths() -> Result<Vec<String>, String> {
4949
Ok(vec![
5050
"test".to_string()

examples/showcase/server/src/main.rs

+13-8
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1-
use actix_web::{web, App, HttpRequest, HttpServer, Result as ActixResult, error};
1+
use actix_web::{web, App, HttpRequest, HttpServer, HttpResponse, http::StatusCode};
22
use actix_files::{NamedFile};
33
use sycamore::prelude::SsrNode;
44
use std::collections::HashMap;
55

66
use perseus::{
77
serve::{get_render_cfg, get_page},
88
config_manager::FsConfigManager,
9-
template::TemplateMap
9+
template::TemplateMap,
10+
errors::ErrorKind as PerseusErr,
11+
errors::err_to_status_code
1012
};
1113
use perseus_showcase_app::pages;
1214

@@ -53,12 +55,15 @@ async fn page_data(
5355
templates: web::Data<TemplateMap<SsrNode>>,
5456
render_cfg: web::Data<HashMap<String, String>>,
5557
config_manager: web::Data<FsConfigManager>
56-
) -> ActixResult<String> {
58+
) -> HttpResponse {
5759
let path = req.match_info().query("filename");
58-
// TODO match different types of errors here
59-
let page_data = get_page(path, &render_cfg, &templates, config_manager.get_ref()).await.map_err(error::ErrorNotFound)?;
60+
let page_data = get_page(path, &render_cfg, &templates, config_manager.get_ref()).await;
61+
let http_res = match page_data {
62+
Ok(page_data) => HttpResponse::Ok().body(
63+
serde_json::to_string(&page_data).unwrap()
64+
),
65+
Err(err) => HttpResponse::build(StatusCode::from_u16(err_to_status_code(&err)).unwrap()).body(err.to_string()),
66+
};
6067

61-
Ok(
62-
serde_json::to_string(&page_data).unwrap()
63-
)
68+
http_res
6469
}

src/errors.rs

+60-7
Original file line numberDiff line numberDiff line change
@@ -3,39 +3,64 @@
33
pub use error_chain::bail;
44
use error_chain::error_chain;
55

6+
/// Defines who caused an ambiguous error message so we can reliably create an HTTP status code. Specific status codes may be provided
7+
/// in either case, or the defaults (400 for client, 500 for server) will be used.
8+
#[derive(Debug)]
9+
pub enum ErrorCause {
10+
Client(Option<u16>),
11+
Server(Option<u16>)
12+
}
13+
14+
// TODO disclose what information may be revealed over the network through these errors in docs
615
// The `error_chain` setup for the whole crate
716
error_chain! {
817
// The custom errors for this crate (very broad)
918
errors {
10-
/// For indistinct JavaScript errors.
19+
/// For indistinct JavaScript errors (potentially sensitive, but only generated on the client-side).
1120
JsErr(err: String) {
1221
description("an error occurred while interfacing with javascript")
1322
display("the following error occurred while interfacing with javascript: {:?}", err)
1423
}
24+
/// For when a fetched URL didn't return a string, which it must.
25+
AssetNotString(url: String) {
26+
description("the fetched asset wasn't a string")
27+
display("the fetched asset at '{}' wasn't a string", url)
28+
}
29+
/// For when the server returned a non-200 error code (not including 404, that's handled separately).
30+
AssetNotOk(url: String, status: u16, err: String) {
31+
description("the asset couldn't be fecthed with a 200 OK")
32+
display("the asset at '{}' returned status code '{}' with payload '{}'", url, status, err)
33+
}
1534

35+
/// For when a necessary template feautre was expected but not present. This just pertains to rendering strategies, and shouldn't
36+
/// ever be sensitive.
1637
TemplateFeatureNotEnabled(name: String, feature: String) {
1738
description("a template feature required by a function called was not present")
1839
display("the template '{}' is missing the feature '{}'", name, feature)
1940
}
41+
/// For when the given path wasn't found, a 404 should never be sensitive.
2042
PageNotFound(path: String) {
2143
description("the requested page was not found")
2244
display("the requested page at path '{}' was not found", path)
2345
}
24-
NoRenderOpts(template_path: String) {
25-
description("a template had no rendering options for use at request-time")
26-
display("the template '{}' had no rendering options for use at request-time", template_path)
27-
}
46+
/// For when the user misconfigured their revalidation length, which should be caught at build time, and hence shouldn't be
47+
/// sensitive.
2848
InvalidDatetimeIntervalIndicator(indicator: String) {
2949
description("invalid indicator in timestring")
3050
display("invalid indicator '{}' in timestring, must be one of: s, m, h, d, w, M, y", indicator)
3151
}
52+
/// For when a template defined both build and request states when it can't amalgamate them sensibly, which indicates a misconfiguration.
53+
/// Revealing the rendering strategies of a template in this way should never be sensitive. Due to the execution context, this
54+
/// doesn't disclose the offending template.
3255
BothStatesDefined {
3356
description("both build and request states were defined for a template when only one or fewer were expected")
3457
display("both build and request states were defined for a template when only one or fewer were expected")
3558
}
36-
RenderFnFailed(fn_name: String, template: String, err_str: String) {
59+
/// For when a render function failed. Only request-time functions can generate errors that will be transmitted over the network,
60+
/// so **render functions must not disclose sensitive information in errors**. Other information shouldn't be sensitive.
61+
RenderFnFailed(fn_name: String, template: String, cause: ErrorCause, err_str: String) {
3762
description("error while calling render function")
38-
display("an error occurred while calling render function '{}' on template '{}': '{}'", fn_name, template, err_str)
63+
display("an error caused by '{:?}' occurred while calling render function '{}' on template '{}': '{}'", cause, fn_name, template, err_str)
3964
}
4065
}
4166
links {
@@ -48,3 +73,31 @@ error_chain! {
4873
ChronoParse(::chrono::ParseError);
4974
}
5075
}
76+
77+
pub fn err_to_status_code(err: &Error) -> u16 {
78+
match err.kind() {
79+
// Misconfiguration
80+
ErrorKind::TemplateFeatureNotEnabled(_, _) => 500,
81+
// Bad request
82+
ErrorKind::PageNotFound(_) => 404,
83+
// Misconfiguration
84+
ErrorKind::InvalidDatetimeIntervalIndicator(_) => 500,
85+
// Misconfiguration
86+
ErrorKind::BothStatesDefined => 500,
87+
// Ambiguous, we'll rely on the given cause
88+
ErrorKind::RenderFnFailed(_, _, cause, _) => match cause {
89+
ErrorCause::Client(code) => code.unwrap_or(400),
90+
ErrorCause::Server(code) => code.unwrap_or(500),
91+
},
92+
// We shouldn't be generating JS errors on the server...
93+
ErrorKind::JsErr(_) => panic!("function 'err_to_status_code' is only intended for server-side usage"),
94+
// These are nearly always server-induced
95+
ErrorKind::ConfigManager(_) => 500,
96+
ErrorKind::Io(_) => 500,
97+
ErrorKind::ChronoParse(_) => 500,
98+
// JSON errors can be caused by the client, but we don't have enough information
99+
ErrorKind::Json(_) => 500,
100+
// Any other errors go to a 500
101+
_ => 500
102+
}
103+
}

0 commit comments

Comments
 (0)