inline problem_details builder at call sites; drop the one-caller wrapper
This commit is contained in:
parent
cdf1bfe7db
commit
2b0c51badf
8 changed files with 79 additions and 78 deletions
|
|
@ -1657,24 +1657,18 @@ fn strip_container_prefix(name: &str) -> String {
|
||||||
.to_owned()
|
.to_owned()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build an RFC 9457 (`application/problem+json`) error response via the
|
/// Convenience wrapper for the common internal-error case: a 500
|
||||||
|
/// RFC 9457 (`application/problem+json`) response via the
|
||||||
/// `problem_details` crate. `from_status_code` sets `status` + `title`
|
/// `problem_details` crate. `from_status_code` sets `status` + `title`
|
||||||
/// (the canonical reason phrase) and leaves `type` as the default
|
/// (the canonical reason phrase) and leaves `type` as the default
|
||||||
/// `about:blank`; `with_detail` carries the caller message. The crate's
|
/// `about:blank`; `with_detail` carries the caller message; the crate's
|
||||||
/// axum `IntoResponse` emits the body with the `application/problem+json`
|
/// axum `IntoResponse` emits the `application/problem+json` body the
|
||||||
/// content type. Centralising this keeps every dashboard error on one
|
/// frontend parses (it reads `detail`). Most dashboard handlers funnel
|
||||||
/// machine-readable shape the frontend parses (it reads `detail`).
|
/// their errors through here; handlers with a more specific client
|
||||||
fn problem_response(status: StatusCode, detail: &str) -> Response {
|
/// failure (bad input, not found) build the same `ProblemDetails`
|
||||||
problem_details::ProblemDetails::from_status_code(status)
|
/// inline with the right status.
|
||||||
.with_detail(detail)
|
fn error_response(message: &str) -> Response {
|
||||||
|
problem_details::ProblemDetails::from_status_code(StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
|
.with_detail(message)
|
||||||
.into_response()
|
.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 {
|
|
||||||
problem_response(StatusCode::INTERNAL_SERVER_ERROR, message)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,9 @@ use axum::{
|
||||||
use hive_sh4re::Approval;
|
use hive_sh4re::Approval;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use super::{AppState, error_response, problem_response};
|
use problem_details::ProblemDetails;
|
||||||
|
|
||||||
|
use super::{AppState, error_response};
|
||||||
use crate::actions;
|
use crate::actions;
|
||||||
use crate::coordinator::Coordinator;
|
use crate::coordinator::Coordinator;
|
||||||
use crate::lifecycle;
|
use crate::lifecycle;
|
||||||
|
|
@ -210,10 +212,9 @@ pub(super) async fn get_approval_diff(
|
||||||
.map(|n| format!("refs/tags/proposal/{n}"))
|
.map(|n| format!("refs/tags/proposal/{n}"))
|
||||||
}
|
}
|
||||||
other => {
|
other => {
|
||||||
return problem_response(
|
return ProblemDetails::from_status_code(StatusCode::BAD_REQUEST)
|
||||||
StatusCode::BAD_REQUEST,
|
.with_detail(format!("unknown diff base {other:?}"))
|
||||||
&format!("unknown diff base {other:?}"),
|
.into_response();
|
||||||
);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let Some(base_ref) = base_ref else {
|
let Some(base_ref) = base_ref else {
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,9 @@ use axum::{
|
||||||
};
|
};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use super::{error_response, problem_response, strip_container_prefix, validate_agent_name};
|
use problem_details::ProblemDetails;
|
||||||
|
|
||||||
|
use super::{error_response, strip_container_prefix, validate_agent_name};
|
||||||
use crate::lifecycle;
|
use crate::lifecycle;
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
|
@ -48,10 +50,9 @@ pub(super) async fn get_journal(
|
||||||
let prefixed = format!("{}{container}", lifecycle::AGENT_PREFIX);
|
let prefixed = format!("{}{container}", lifecycle::AGENT_PREFIX);
|
||||||
let live = lifecycle::list().await.unwrap_or_default();
|
let live = lifecycle::list().await.unwrap_or_default();
|
||||||
if !live.iter().any(|c| c == &prefixed) {
|
if !live.iter().any(|c| c == &prefixed) {
|
||||||
return problem_response(
|
return ProblemDetails::from_status_code(StatusCode::NOT_FOUND)
|
||||||
StatusCode::NOT_FOUND,
|
.with_detail(format!("journal: no managed container {prefixed:?}"))
|
||||||
&format!("journal: no managed container {prefixed:?}"),
|
.into_response();
|
||||||
);
|
|
||||||
}
|
}
|
||||||
let lines = q.lines.unwrap_or(500).min(5000);
|
let lines = q.lines.unwrap_or(500).min(5000);
|
||||||
let unit = match q.unit.as_deref().filter(|s| !s.is_empty()) {
|
let unit = match q.unit.as_deref().filter(|s| !s.is_empty()) {
|
||||||
|
|
@ -64,10 +65,9 @@ pub(super) async fn get_journal(
|
||||||
format!("{u}.service")
|
format!("{u}.service")
|
||||||
};
|
};
|
||||||
if !allowed.contains(&unit.as_str()) {
|
if !allowed.contains(&unit.as_str()) {
|
||||||
return problem_response(
|
return ProblemDetails::from_status_code(StatusCode::BAD_REQUEST)
|
||||||
StatusCode::BAD_REQUEST,
|
.with_detail(format!("journal: unknown unit {unit:?}"))
|
||||||
&format!("journal: unknown unit {unit:?}"),
|
.into_response();
|
||||||
);
|
|
||||||
}
|
}
|
||||||
Some(unit)
|
Some(unit)
|
||||||
}
|
}
|
||||||
|
|
@ -127,10 +127,9 @@ pub(super) async fn get_journal_host(
|
||||||
format!("{u}.service")
|
format!("{u}.service")
|
||||||
};
|
};
|
||||||
if !allowed.contains(&unit.as_str()) {
|
if !allowed.contains(&unit.as_str()) {
|
||||||
return problem_response(
|
return ProblemDetails::from_status_code(StatusCode::BAD_REQUEST)
|
||||||
StatusCode::BAD_REQUEST,
|
.with_detail(format!("journal-host: unknown unit {unit:?}"))
|
||||||
&format!("journal-host: unknown unit {unit:?}"),
|
.into_response();
|
||||||
);
|
|
||||||
}
|
}
|
||||||
cmd.args(["-u", &unit]);
|
cmd.args(["-u", &unit]);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,9 @@ use axum::{
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use super::{AppState, guard_agent_name, problem_response, strip_container_prefix};
|
use problem_details::ProblemDetails;
|
||||||
|
|
||||||
|
use super::{AppState, guard_agent_name, strip_container_prefix};
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
pub(super) struct ToolGroupsSnapshot {
|
pub(super) struct ToolGroupsSnapshot {
|
||||||
|
|
@ -120,10 +122,9 @@ pub(super) async fn post_tool_groups(
|
||||||
// Validate group names before queuing — fail fast so the operator
|
// Validate group names before queuing — fail fast so the operator
|
||||||
// sees the error immediately rather than waiting for the worker.
|
// sees the error immediately rather than waiting for the worker.
|
||||||
if let Err(e) = crate::tool_groups::validate_groups(&body.groups) {
|
if let Err(e) = crate::tool_groups::validate_groups(&body.groups) {
|
||||||
return problem_response(
|
return ProblemDetails::from_status_code(StatusCode::BAD_REQUEST)
|
||||||
StatusCode::BAD_REQUEST,
|
.with_detail(format!("invalid tool-groups for {logical}: {e}"))
|
||||||
&format!("invalid tool-groups for {logical}: {e}"),
|
.into_response();
|
||||||
);
|
|
||||||
}
|
}
|
||||||
// Enqueue a PermChange so the JSON file write is serialised through
|
// Enqueue a PermChange so the JSON file write is serialised through
|
||||||
// the FIFO worker. Prevents concurrent batch-apply actions for
|
// the FIFO worker. Prevents concurrent batch-apply actions for
|
||||||
|
|
@ -207,10 +208,9 @@ pub(super) async fn post_capabilities(
|
||||||
.collect();
|
.collect();
|
||||||
for cap in &body.caps {
|
for cap in &body.caps {
|
||||||
if !known.contains(&cap.as_str()) {
|
if !known.contains(&cap.as_str()) {
|
||||||
return problem_response(
|
return ProblemDetails::from_status_code(StatusCode::BAD_REQUEST)
|
||||||
StatusCode::BAD_REQUEST,
|
.with_detail(format!("unknown capability: {cap}"))
|
||||||
&format!("unknown capability: {cap}"),
|
.into_response();
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Enqueue a PermChange so the JSON file write is serialised through
|
// Enqueue a PermChange so the JSON file write is serialised through
|
||||||
|
|
@ -278,18 +278,16 @@ pub(super) async fn post_permissions(
|
||||||
if let Some(groups) = &change.tool_groups
|
if let Some(groups) = &change.tool_groups
|
||||||
&& let Err(e) = crate::tool_groups::validate_groups(groups)
|
&& let Err(e) = crate::tool_groups::validate_groups(groups)
|
||||||
{
|
{
|
||||||
return problem_response(
|
return ProblemDetails::from_status_code(StatusCode::BAD_REQUEST)
|
||||||
StatusCode::BAD_REQUEST,
|
.with_detail(format!("invalid tool-groups for {logical}: {e}"))
|
||||||
&format!("invalid tool-groups for {logical}: {e}"),
|
.into_response();
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if let Some(caps) = &change.capabilities {
|
if let Some(caps) = &change.capabilities {
|
||||||
for cap in caps {
|
for cap in caps {
|
||||||
if !known_caps.contains(&cap.as_str()) {
|
if !known_caps.contains(&cap.as_str()) {
|
||||||
return problem_response(
|
return ProblemDetails::from_status_code(StatusCode::BAD_REQUEST)
|
||||||
StatusCode::BAD_REQUEST,
|
.with_detail(format!("unknown capability for {logical}: {cap}"))
|
||||||
&format!("unknown capability for {logical}: {cap}"),
|
.into_response();
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,9 @@ use axum::{
|
||||||
};
|
};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use super::{AppState, error_response, problem_response};
|
use problem_details::ProblemDetails;
|
||||||
|
|
||||||
|
use super::{AppState, error_response};
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub(super) struct AnswerForm {
|
pub(super) struct AnswerForm {
|
||||||
|
|
@ -41,10 +43,11 @@ pub(super) async fn post_answer_question(
|
||||||
) -> Response {
|
) -> Response {
|
||||||
let answer = form.answer.trim();
|
let answer = form.answer.trim();
|
||||||
if answer.is_empty() {
|
if answer.is_empty() {
|
||||||
return with_cors(problem_response(
|
return with_cors(
|
||||||
StatusCode::BAD_REQUEST,
|
ProblemDetails::from_status_code(StatusCode::BAD_REQUEST)
|
||||||
"answer: required",
|
.with_detail("answer: required")
|
||||||
));
|
.into_response(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
let resp = match state
|
let resp = match state
|
||||||
.coord
|
.coord
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,9 @@ use axum::{
|
||||||
response::{IntoResponse, Response},
|
response::{IntoResponse, Response},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{AppState, error_response, problem_response};
|
use problem_details::ProblemDetails;
|
||||||
|
|
||||||
|
use super::{AppState, error_response};
|
||||||
|
|
||||||
pub(super) async fn api_reminders(State(state): State<AppState>) -> Response {
|
pub(super) async fn api_reminders(State(state): State<AppState>) -> Response {
|
||||||
match state.coord.broker.list_pending_reminders() {
|
match state.coord.broker.list_pending_reminders() {
|
||||||
|
|
@ -24,10 +26,9 @@ pub(super) async fn post_cancel_reminder(
|
||||||
AxumPath(id): AxumPath<i64>,
|
AxumPath(id): AxumPath<i64>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
match state.coord.broker.cancel_reminder(id) {
|
match state.coord.broker.cancel_reminder(id) {
|
||||||
Ok(0) => problem_response(
|
Ok(0) => ProblemDetails::from_status_code(StatusCode::NOT_FOUND)
|
||||||
StatusCode::NOT_FOUND,
|
.with_detail(format!("reminder {id} not pending (already delivered?)"))
|
||||||
&format!("reminder {id} not pending (already delivered?)"),
|
.into_response(),
|
||||||
),
|
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
tracing::info!(%id, "operator cancelled reminder");
|
tracing::info!(%id, "operator cancelled reminder");
|
||||||
state.coord.emit_reminders_snapshot();
|
state.coord.emit_reminders_snapshot();
|
||||||
|
|
@ -47,10 +48,9 @@ pub(super) async fn post_retry_reminder(
|
||||||
AxumPath(id): AxumPath<i64>,
|
AxumPath(id): AxumPath<i64>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
match state.coord.broker.reset_reminder_failure(id) {
|
match state.coord.broker.reset_reminder_failure(id) {
|
||||||
Ok(0) => problem_response(
|
Ok(0) => ProblemDetails::from_status_code(StatusCode::NOT_FOUND)
|
||||||
StatusCode::NOT_FOUND,
|
.with_detail(format!("reminder {id} not pending (already delivered?)"))
|
||||||
&format!("reminder {id} not pending (already delivered?)"),
|
.into_response(),
|
||||||
),
|
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
tracing::info!(%id, "operator reset reminder failure for retry");
|
tracing::info!(%id, "operator reset reminder failure for retry");
|
||||||
state.coord.emit_reminders_snapshot();
|
state.coord.emit_reminders_snapshot();
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,9 @@ use axum::{
|
||||||
response::{IntoResponse, Response},
|
response::{IntoResponse, Response},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{AppState, error_response, problem_response};
|
use problem_details::ProblemDetails;
|
||||||
|
|
||||||
|
use super::{AppState, error_response};
|
||||||
|
|
||||||
/// `GET /api/schedules` — snapshot of every schedule for the
|
/// `GET /api/schedules` — snapshot of every schedule for the
|
||||||
/// scheduled-prompts tab. Returns the wire shape directly
|
/// scheduled-prompts tab. Returns the wire shape directly
|
||||||
|
|
@ -51,19 +53,19 @@ pub(super) async fn post_schedule_new(
|
||||||
axum::Json(payload): axum::Json<hive_sh4re::SchedulePromptPayload>,
|
axum::Json(payload): axum::Json<hive_sh4re::SchedulePromptPayload>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
if payload.targets.is_empty() {
|
if payload.targets.is_empty() {
|
||||||
return problem_response(
|
return ProblemDetails::from_status_code(StatusCode::BAD_REQUEST)
|
||||||
StatusCode::BAD_REQUEST,
|
.with_detail("schedule must have at least one target")
|
||||||
"schedule must have at least one target",
|
.into_response();
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if payload.body.trim().is_empty() {
|
if payload.body.trim().is_empty() {
|
||||||
return problem_response(StatusCode::BAD_REQUEST, "schedule body must be non-empty");
|
return ProblemDetails::from_status_code(StatusCode::BAD_REQUEST)
|
||||||
|
.with_detail("schedule body must be non-empty")
|
||||||
|
.into_response();
|
||||||
}
|
}
|
||||||
if let Some(0) = payload.interval_seconds {
|
if let Some(0) = payload.interval_seconds {
|
||||||
return problem_response(
|
return ProblemDetails::from_status_code(StatusCode::BAD_REQUEST)
|
||||||
StatusCode::BAD_REQUEST,
|
.with_detail("interval_seconds must be > 0 (use None for one-shot)")
|
||||||
"interval_seconds must be > 0 (use None for one-shot)",
|
.into_response();
|
||||||
);
|
|
||||||
}
|
}
|
||||||
let new = crate::scheduled_prompts::NewSchedule {
|
let new = crate::scheduled_prompts::NewSchedule {
|
||||||
owner: hive_sh4re::OPERATOR_RECIPIENT.to_owned(),
|
owner: hive_sh4re::OPERATOR_RECIPIENT.to_owned(),
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,9 @@ use axum::{
|
||||||
};
|
};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use super::{AppState, error_response, problem_response};
|
use problem_details::ProblemDetails;
|
||||||
|
|
||||||
|
use super::{AppState, error_response};
|
||||||
|
|
||||||
/// `POST /api/topology/set-parent` body. `child` is required.
|
/// `POST /api/topology/set-parent` body. `child` is required.
|
||||||
/// `new_parent` may be:
|
/// `new_parent` may be:
|
||||||
|
|
@ -53,7 +55,9 @@ pub(super) async fn post_set_parent(
|
||||||
) -> Response {
|
) -> Response {
|
||||||
let child = form.child.trim().to_owned();
|
let child = form.child.trim().to_owned();
|
||||||
if child.is_empty() {
|
if child.is_empty() {
|
||||||
return problem_response(StatusCode::BAD_REQUEST, "set-parent: `child` required");
|
return ProblemDetails::from_status_code(StatusCode::BAD_REQUEST)
|
||||||
|
.with_detail("set-parent: `child` required")
|
||||||
|
.into_response();
|
||||||
}
|
}
|
||||||
// Empty / whitespace-only `new_parent` ⇒ promote to root. Web
|
// Empty / whitespace-only `new_parent` ⇒ promote to root. Web
|
||||||
// forms submit the empty string for a "no value" radio button,
|
// forms submit the empty string for a "no value" radio button,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue