fix: htmx aware error handling

This commit is contained in:
Per Stark
2025-04-08 16:12:08 +02:00
parent 1d7ec09e32
commit 2cafcce9ad
3 changed files with 38 additions and 39 deletions
@@ -4,6 +4,7 @@ use axum::{
response::{Html, IntoResponse, Response}, response::{Html, IntoResponse, Response},
Extension, Extension,
}; };
use axum_htmx::{HxRequest, HX_TRIGGER};
use common::{error::AppError, utils::template_engine::ProvidesTemplateEngine}; use common::{error::AppError, utils::template_engine::ProvidesTemplateEngine};
use minijinja::{context, Value}; use minijinja::{context, Value};
use serde::Serialize; use serde::Serialize;
@@ -97,7 +98,11 @@ impl IntoResponse for TemplateResponse {
} }
} }
pub async fn with_template_response<S>(State(state): State<S>, response: Response) -> Response pub async fn with_template_response<S>(
State(state): State<S>,
HxRequest(is_htmx): HxRequest,
response: Response,
) -> Response
where where
S: ProvidesTemplateEngine + Clone + Send + Sync + 'static, S: ProvidesTemplateEngine + Clone + Send + Sync + 'static,
{ {
@@ -123,44 +128,37 @@ where
} }
} }
} }
TemplateKind::Error(_status) => { TemplateKind::Error(status) => {
// Extract title and description from context if is_htmx {
let title = template_response // HTMX request: Send 204 + HX-Trigger for toast
.context let title = template_response
.get_attr("title") .context
.ok() .get_attr("title")
.and_then(|v| v.as_str().map(|s| s.to_string())) .ok()
.unwrap_or_else(|| "Error".to_string()); // Fallback title .and_then(|v| v.as_str().map(String::from))
let description = template_response .unwrap_or_else(|| "Error".to_string());
.context let description = template_response
.get_attr("description") .context
.ok() .get_attr("description")
.and_then(|v| v.as_str().map(|s| s.to_string())) .ok()
.unwrap_or_else(|| "An error occurred.".to_string()); // Fallback desc .and_then(|v| v.as_str().map(String::from))
.unwrap_or_else(|| "An error occurred.".to_string());
let trigger_payload = json!({ let trigger_payload = json!({"toast": {"title": title, "description": description, "type": "error"}});
"toast": { let trigger_value = serde_json::to_string(&trigger_payload).unwrap_or_else(|e| {error!("Failed to serialize HX-Trigger payload: {}", e);
"title": title, r#"{"toast":{"title":"Error","description":"An unexpected error occurred.", "type":"error"}}"#.to_string()});
"description": description, (StatusCode::NO_CONTENT, [(HX_TRIGGER, trigger_value)], "").into_response()
"type": "error" } else {
// Non-HTMX request: Render the full errors/error.html page
match template_engine.render("errors/error.html", &template_response.context) {
Ok(html) => (*status, Html(html)).into_response(),
Err(e) => {
error!("Critical: Failed to render 'errors/error.html': {:?}", e);
// Fallback HTML, but use the intended status code
(*status, Html(fallback_error())).into_response()
}
} }
}); }
// Convert payload to string
let trigger_value = serde_json::to_string(&trigger_payload)
.unwrap_or_else(|e| {
error!("Failed to serialize HX-Trigger payload: {}", e);
// Fallback trigger if serialization fails
r#"{"toast":{"title":"Error","description":"An unexpected error occurred.", "type":"error"}}"#.to_string()
});
// Return 204 No Content with HX-Trigger header
(
StatusCode::NO_CONTENT,
[(axum_htmx::HX_TRIGGER, trigger_value)],
"", // Empty body for 204
)
.into_response()
} }
TemplateKind::Redirect(path) => { TemplateKind::Redirect(path) => {
(StatusCode::OK, [(axum_htmx::HX_REDIRECT, path.clone())], "").into_response() (StatusCode::OK, [(axum_htmx::HX_REDIRECT, path.clone())], "").into_response()
+1
View File
@@ -151,6 +151,7 @@ pub async fn show_active_jobs(
State(state): State<HtmlState>, State(state): State<HtmlState>,
RequireUser(user): RequireUser, RequireUser(user): RequireUser,
) -> Result<impl IntoResponse, HtmlError> { ) -> Result<impl IntoResponse, HtmlError> {
return Ok(TemplateResponse::server_error());
let active_jobs = User::get_unfinished_ingestion_tasks(&user.id, &state.db).await?; let active_jobs = User::get_unfinished_ingestion_tasks(&user.id, &state.db).await?;
Ok(TemplateResponse::new_partial( Ok(TemplateResponse::new_partial(
+2 -2
View File
@@ -1,11 +1,11 @@
{% extends 'body_base.html' %} {% extends 'body_base.html' %}
{% block main %} {% block main %}
<main class="container justify-center flex-grow flex mx-auto mt-4"> <main class="container justify-center flex-grow flex mx-auto mt-4">
<div class="flex flex-col space-y-4 text-center"> <div class="flex flex-col space-y-4 text-center justify-center">
<h1 class="text-2xl font-bold text-error"> <h1 class="text-2xl font-bold text-error">
{{ status_code }} {{ status_code }}
</h1> </h1>
<p class="text-2xl my-4">{{ error }}</p> <p class="text-2xl my-4">{{ title }}</p>
<p class="text-base-content/60">{{ description }}</p> <p class="text-base-content/60">{{ description }}</p>
<a href="/" class="btn btn-primary mt-8">Go Home</a> <a href="/" class="btn btn-primary mt-8">Go Home</a>
</div> </div>