emit dashboard errors as rfc 9457 problem+json

This commit is contained in:
damocles 2026-06-22 13:46:05 +02:00 committed by mara
commit b11360503a

View file

@ -1131,6 +1131,23 @@ struct RequestSpawnForm {
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn problem_body_has_rfc9457_members() {
// about:blank type → title is the canonical status reason phrase,
// status is the numeric code, detail is the caller message.
let body = problem_body(StatusCode::BAD_REQUEST, "bad input");
assert_eq!(body["type"], "about:blank");
assert_eq!(body["title"], "Bad Request");
assert_eq!(body["status"], 400);
assert_eq!(body["detail"], "bad input");
// error_response (the 500 wrapper) carries the same shape with the
// internal-error status.
let five = problem_body(StatusCode::INTERNAL_SERVER_ERROR, "boom");
assert_eq!(five["status"], 500);
assert_eq!(five["title"], "Internal Server Error");
}
#[test]
fn walk_meta_inputs_keeps_nixpkgs_under_hyperhive_post_follows_refactor() {
// Reproduce the shape where meta has
@ -1638,8 +1655,44 @@ fn strip_container_prefix(name: &str) -> String {
.to_owned()
}
/// The RFC 9457 problem-details media type.
const PROBLEM_JSON_CONTENT_TYPE: &str = "application/problem+json";
/// Build the RFC 9457 problem-details body for `status` + `detail`. The
/// object carries the standard members: `type` ("about:blank", i.e. no
/// problem-specific type), `title` (the HTTP status reason phrase),
/// `status` (numeric code) and `detail` (the caller-supplied message).
/// Split from [`problem_response`] so the member shape is unit-testable
/// without axum response plumbing.
fn problem_body(status: StatusCode, detail: &str) -> serde_json::Value {
serde_json::json!({
"type": "about:blank",
"title": status.canonical_reason().unwrap_or("Error"),
"status": status.as_u16(),
"detail": detail,
})
}
/// Build an RFC 9457 (`application/problem+json`) error response.
/// Centralising this keeps every dashboard error on one machine-readable
/// shape the frontend can parse (read `detail` for display) instead of
/// guessing between plain text and JSON.
fn problem_response(status: StatusCode, detail: &str) -> Response {
let body = serde_json::to_string(&problem_body(status, detail))
.expect("problem+json body is always serialisable");
(
status,
[(axum::http::header::CONTENT_TYPE, PROBLEM_JSON_CONTENT_TYPE)],
body,
)
.into_response()
}
/// Convenience wrapper for the common internal-error case: a 500
/// problem-details response (see [`problem_response`]). Most dashboard
/// handlers funnel their errors through here; handlers with a more
/// specific failure (bad input, not found) call [`problem_response`]
/// directly with the right status.
fn error_response(message: &str) -> Response {
// Plain text — the JS app surfaces this in an alert(), so HTML
// wrapping would just clutter the message.
(StatusCode::INTERNAL_SERVER_ERROR, message.to_owned()).into_response()
problem_response(StatusCode::INTERNAL_SERVER_ERROR, message)
}