Improve passing of errors from higher functions to template rendering

This commit is contained in:
Juno Takano 2025-12-11 02:20:46 -03:00
commit b306f04664
2 changed files with 63 additions and 41 deletions

View file

@ -1,7 +1,7 @@
use axum::{ use axum::{
extract::Path, extract::Path,
http::{header, StatusCode}, http::{header, StatusCode},
response::{ Html, IntoResponse, Redirect }, response::{IntoResponse, Redirect},
routing::get, routing::get,
Form, Form,
Router, Router,
@ -74,40 +74,62 @@ fn make_body(
} }
}; };
let render_result = match tera.render(name, &context) { match tera.render(name, context) {
Ok(t) => t, Ok(t) => (t, 200),
Err(e) => { Err(e) => {
let mut error_context = tera::Context::new(); let mut error_context = tera::Context::new();
let error = StatusCode::from_u16(error_code)
.unwrap_or(StatusCode::NOT_IMPLEMENTED);
error_context.insert("title", &error.to_string());
error_context.insert(
"message",
&format!(
r#"<strong>Error while filling template {name}:</strong> {}
<strong>User message:</strong> {error_message}"#,
e.to_string(),
),
);
tera.render("error.html", &error_context) let out_error_message = match error_message {
.unwrap_or(error_message.to_string()) Some(s) => &format!(
"Template render failed.\n\
User message: {s},
Engine message:\n<pre>{e:#?}</pre>\n\
Context:\n<pre>{context:#?}</pre>"
),
None => {
&format!(
"Template render failed.\n\
Engine message:\n<pre>{e:#?}</pre>\n\
Context:\n<pre>{context:#?}</pre>"
)
} }
}; };
render_result error_context.insert("message", out_error_message);
} error_context.insert("title",
&StatusCode::INTERNAL_SERVER_ERROR.to_string());
(tera.render("error.html", &error_context)
.unwrap_or(out_error_message.clone()), 500)
}
}
}
fn template_handler( fn template_handler(
name: &str, name: &str,
context: tera::Context, context: tera::Context,
error_code: u16, error_code: u16,
error_message: &str, error_message: Option<String>,
) -> Html<String> { is_error: bool,
let body = make_body(name, context, error_code, error_message); ) -> impl IntoResponse {
Html(body)
let (body, render_status) = make_body(
name, &context, error_message.as_deref());
let status_code = if render_status != 200 {
StatusCode::from_u16(render_status)
.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)
} else if is_error {
StatusCode::from_u16(error_code)
.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)
} else { StatusCode::OK };
(
status_code,
[(header::CONTENT_TYPE, "text/html")],
body.clone(),
)
} }
async fn node_view(Path(id): Path<String>) -> impl IntoResponse { async fn node_view(Path(id): Path<String>) -> impl IntoResponse {
@ -128,23 +150,22 @@ async fn node_view(Path(id): Path<String>) -> impl IntoResponse {
context.insert("connections", &node.connections.clone()); context.insert("connections", &node.connections.clone());
context.insert("incoming", &graph.incoming.get(&id)); context.insert("incoming", &graph.incoming.get(&id));
let not_found = node.clone() == empty_node;
template_handler( template_handler(
"node.html", "node.html",
context, context,
500, if not_found { 404 } else { 500 },
&format!( Some(format!(
r#"Failed to generate page for node {} (ID {}) with {} outgoing, "Failed to generate page for node {} (ID {}).\n\
{} incoming connections and text "{}""#, Node struct: <pre>{:#?}</pre>",
node.title, node.title, id, node
id, ).to_owned()),
node.connections.iter().len(), not_found,
graph.incoming.get(&id).iter().len(),
node.text,
),
) )
} }
async fn index() -> Html<String> { async fn index() -> impl IntoResponse {
let mut context = tera::Context::new(); let mut context = tera::Context::new();
let graph = populate_graph(); let graph = populate_graph();
@ -154,10 +175,10 @@ async fn index() -> Html<String> {
context.insert("nodes", &nodes); context.insert("nodes", &nodes);
context.insert("root_node", &root_node); context.insert("root_node", &root_node);
template_handler("index.html", context, 500, "Failed to render template.") template_handler("index.html", context.clone(), 500, None, false)
} }
async fn tree() -> Html<String> { async fn tree() -> impl IntoResponse {
let mut context = tera::Context::new(); let mut context = tera::Context::new();
let graph = populate_graph(); let graph = populate_graph();
@ -167,7 +188,8 @@ async fn tree() -> Html<String> {
context.insert("nodes", &nodes); context.insert("nodes", &nodes);
context.insert("root_node", &root_node); context.insert("root_node", &root_node);
template_handler("tree.html", context, 500, "Failed to render template") template_handler("tree.html", context, 500, None, false)
}
#[expect(clippy::unused_async)] #[expect(clippy::unused_async)]
async fn static_template_handler(name: &str) -> impl IntoResponse { async fn static_template_handler(name: &str) -> impl IntoResponse {

View file

@ -1,7 +1,7 @@
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
use std::collections::HashMap; use std::collections::HashMap;
#[derive(Serialize, Deserialize, Clone, Default, Debug)] #[derive(Serialize, Deserialize, Clone, Default, PartialEq, Eq, Debug)]
pub struct Graph { pub struct Graph {
pub nodes: HashMap<String, Node>, pub nodes: HashMap<String, Node>,
pub root_node: String, pub root_node: String,
@ -9,7 +9,7 @@ pub struct Graph {
#[serde(skip)] pub incoming: HashMap<String, Vec<Edge>>, #[serde(skip)] pub incoming: HashMap<String, Vec<Edge>>,
} }
#[derive(Serialize, Deserialize, Clone, Default, Debug)] #[derive(Serialize, Deserialize, Clone, Default, PartialEq, Eq, Debug)]
pub struct Node { pub struct Node {
pub text: String, pub text: String,
#[serde(default)] pub title: String, #[serde(default)] pub title: String,
@ -20,7 +20,7 @@ pub struct Node {
pub connections: Option<Vec<Edge>>, pub connections: Option<Vec<Edge>>,
} }
#[derive(Serialize, Clone, Default, PartialEq, Deserialize, Debug)] #[derive(Serialize, Deserialize, Clone, Default, PartialEq, Eq, Debug)]
pub struct Edge { pub struct Edge {
pub to: String, pub to: String,
#[serde(default)] pub anchor: String, #[serde(default)] pub anchor: String,