return accurate http status codes for dashboard client errors
This commit is contained in:
parent
dc4c5460d5
commit
5dc1b3933a
7 changed files with 63 additions and 22 deletions
|
|
@ -16,7 +16,7 @@ use axum::{
|
|||
use hive_sh4re::Approval;
|
||||
use serde::Deserialize;
|
||||
|
||||
use super::{AppState, error_response};
|
||||
use super::{AppState, error_response, problem_response};
|
||||
use crate::actions;
|
||||
use crate::coordinator::Coordinator;
|
||||
use crate::lifecycle;
|
||||
|
|
@ -209,7 +209,12 @@ pub(super) async fn get_approval_diff(
|
|||
.max()
|
||||
.map(|n| format!("refs/tags/proposal/{n}"))
|
||||
}
|
||||
other => return error_response(&format!("unknown diff base {other:?}")),
|
||||
other => {
|
||||
return problem_response(
|
||||
StatusCode::BAD_REQUEST,
|
||||
&format!("unknown diff base {other:?}"),
|
||||
);
|
||||
}
|
||||
};
|
||||
let Some(base_ref) = base_ref else {
|
||||
return plain_text(match base {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ use axum::{
|
|||
};
|
||||
use serde::Deserialize;
|
||||
|
||||
use super::{error_response, strip_container_prefix, validate_agent_name};
|
||||
use super::{error_response, problem_response, strip_container_prefix, validate_agent_name};
|
||||
use crate::lifecycle;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
|
|
@ -48,7 +48,10 @@ pub(super) async fn get_journal(
|
|||
let prefixed = format!("{}{container}", lifecycle::AGENT_PREFIX);
|
||||
let live = lifecycle::list().await.unwrap_or_default();
|
||||
if !live.iter().any(|c| c == &prefixed) {
|
||||
return error_response(&format!("journal: no managed container {prefixed:?}"));
|
||||
return problem_response(
|
||||
StatusCode::NOT_FOUND,
|
||||
&format!("journal: no managed container {prefixed:?}"),
|
||||
);
|
||||
}
|
||||
let lines = q.lines.unwrap_or(500).min(5000);
|
||||
let unit = match q.unit.as_deref().filter(|s| !s.is_empty()) {
|
||||
|
|
@ -61,7 +64,10 @@ pub(super) async fn get_journal(
|
|||
format!("{u}.service")
|
||||
};
|
||||
if !allowed.contains(&unit.as_str()) {
|
||||
return error_response(&format!("journal: unknown unit {unit:?}"));
|
||||
return problem_response(
|
||||
StatusCode::BAD_REQUEST,
|
||||
&format!("journal: unknown unit {unit:?}"),
|
||||
);
|
||||
}
|
||||
Some(unit)
|
||||
}
|
||||
|
|
@ -121,7 +127,10 @@ pub(super) async fn get_journal_host(
|
|||
format!("{u}.service")
|
||||
};
|
||||
if !allowed.contains(&unit.as_str()) {
|
||||
return error_response(&format!("journal-host: unknown unit {unit:?}"));
|
||||
return problem_response(
|
||||
StatusCode::BAD_REQUEST,
|
||||
&format!("journal-host: unknown unit {unit:?}"),
|
||||
);
|
||||
}
|
||||
cmd.args(["-u", &unit]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ use axum::{
|
|||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::{AppState, error_response, guard_agent_name, strip_container_prefix};
|
||||
use super::{AppState, guard_agent_name, problem_response, strip_container_prefix};
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(super) struct ToolGroupsSnapshot {
|
||||
|
|
@ -120,7 +120,10 @@ pub(super) async fn post_tool_groups(
|
|||
// Validate group names before queuing — fail fast so the operator
|
||||
// sees the error immediately rather than waiting for the worker.
|
||||
if let Err(e) = crate::tool_groups::validate_groups(&body.groups) {
|
||||
return error_response(&format!("invalid tool-groups for {logical}: {e}"));
|
||||
return problem_response(
|
||||
StatusCode::BAD_REQUEST,
|
||||
&format!("invalid tool-groups for {logical}: {e}"),
|
||||
);
|
||||
}
|
||||
// Enqueue a PermChange so the JSON file write is serialised through
|
||||
// the FIFO worker. Prevents concurrent batch-apply actions for
|
||||
|
|
@ -204,7 +207,10 @@ pub(super) async fn post_capabilities(
|
|||
.collect();
|
||||
for cap in &body.caps {
|
||||
if !known.contains(&cap.as_str()) {
|
||||
return error_response(&format!("unknown capability: {cap}"));
|
||||
return problem_response(
|
||||
StatusCode::BAD_REQUEST,
|
||||
&format!("unknown capability: {cap}"),
|
||||
);
|
||||
}
|
||||
}
|
||||
// Enqueue a PermChange so the JSON file write is serialised through
|
||||
|
|
@ -272,12 +278,18 @@ pub(super) async fn post_permissions(
|
|||
if let Some(groups) = &change.tool_groups
|
||||
&& let Err(e) = crate::tool_groups::validate_groups(groups)
|
||||
{
|
||||
return error_response(&format!("invalid tool-groups for {logical}: {e}"));
|
||||
return problem_response(
|
||||
StatusCode::BAD_REQUEST,
|
||||
&format!("invalid tool-groups for {logical}: {e}"),
|
||||
);
|
||||
}
|
||||
if let Some(caps) = &change.capabilities {
|
||||
for cap in caps {
|
||||
if !known_caps.contains(&cap.as_str()) {
|
||||
return error_response(&format!("unknown capability for {logical}: {cap}"));
|
||||
return problem_response(
|
||||
StatusCode::BAD_REQUEST,
|
||||
&format!("unknown capability for {logical}: {cap}"),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ use axum::{
|
|||
};
|
||||
use serde::Deserialize;
|
||||
|
||||
use super::{AppState, error_response};
|
||||
use super::{AppState, error_response, problem_response};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub(super) struct AnswerForm {
|
||||
|
|
@ -41,7 +41,10 @@ pub(super) async fn post_answer_question(
|
|||
) -> Response {
|
||||
let answer = form.answer.trim();
|
||||
if answer.is_empty() {
|
||||
return with_cors(error_response("answer: required"));
|
||||
return with_cors(problem_response(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"answer: required",
|
||||
));
|
||||
}
|
||||
let resp = match state
|
||||
.coord
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ use axum::{
|
|||
response::{IntoResponse, Response},
|
||||
};
|
||||
|
||||
use super::{AppState, error_response};
|
||||
use super::{AppState, error_response, problem_response};
|
||||
|
||||
pub(super) async fn api_reminders(State(state): State<AppState>) -> Response {
|
||||
match state.coord.broker.list_pending_reminders() {
|
||||
|
|
@ -24,7 +24,10 @@ pub(super) async fn post_cancel_reminder(
|
|||
AxumPath(id): AxumPath<i64>,
|
||||
) -> Response {
|
||||
match state.coord.broker.cancel_reminder(id) {
|
||||
Ok(0) => error_response(&format!("reminder {id} not pending (already delivered?)")),
|
||||
Ok(0) => problem_response(
|
||||
StatusCode::NOT_FOUND,
|
||||
&format!("reminder {id} not pending (already delivered?)"),
|
||||
),
|
||||
Ok(_) => {
|
||||
tracing::info!(%id, "operator cancelled reminder");
|
||||
state.coord.emit_reminders_snapshot();
|
||||
|
|
@ -44,7 +47,10 @@ pub(super) async fn post_retry_reminder(
|
|||
AxumPath(id): AxumPath<i64>,
|
||||
) -> Response {
|
||||
match state.coord.broker.reset_reminder_failure(id) {
|
||||
Ok(0) => error_response(&format!("reminder {id} not pending (already delivered?)")),
|
||||
Ok(0) => problem_response(
|
||||
StatusCode::NOT_FOUND,
|
||||
&format!("reminder {id} not pending (already delivered?)"),
|
||||
),
|
||||
Ok(_) => {
|
||||
tracing::info!(%id, "operator reset reminder failure for retry");
|
||||
state.coord.emit_reminders_snapshot();
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ use axum::{
|
|||
response::{IntoResponse, Response},
|
||||
};
|
||||
|
||||
use super::{AppState, error_response};
|
||||
use super::{AppState, error_response, problem_response};
|
||||
|
||||
/// `GET /api/schedules` — snapshot of every schedule for the
|
||||
/// scheduled-prompts tab. Returns the wire shape directly
|
||||
|
|
@ -51,13 +51,19 @@ pub(super) async fn post_schedule_new(
|
|||
axum::Json(payload): axum::Json<hive_sh4re::SchedulePromptPayload>,
|
||||
) -> Response {
|
||||
if payload.targets.is_empty() {
|
||||
return error_response("schedule must have at least one target");
|
||||
return problem_response(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"schedule must have at least one target",
|
||||
);
|
||||
}
|
||||
if payload.body.trim().is_empty() {
|
||||
return error_response("schedule body must be non-empty");
|
||||
return problem_response(StatusCode::BAD_REQUEST, "schedule body must be non-empty");
|
||||
}
|
||||
if let Some(0) = payload.interval_seconds {
|
||||
return error_response("interval_seconds must be > 0 (use None for one-shot)");
|
||||
return problem_response(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"interval_seconds must be > 0 (use None for one-shot)",
|
||||
);
|
||||
}
|
||||
let new = crate::scheduled_prompts::NewSchedule {
|
||||
owner: hive_sh4re::OPERATOR_RECIPIENT.to_owned(),
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ use axum::{
|
|||
};
|
||||
use serde::Deserialize;
|
||||
|
||||
use super::{AppState, error_response};
|
||||
use super::{AppState, error_response, problem_response};
|
||||
|
||||
/// `POST /api/topology/set-parent` body. `child` is required.
|
||||
/// `new_parent` may be:
|
||||
|
|
@ -53,7 +53,7 @@ pub(super) async fn post_set_parent(
|
|||
) -> Response {
|
||||
let child = form.child.trim().to_owned();
|
||||
if child.is_empty() {
|
||||
return error_response("set-parent: `child` required");
|
||||
return problem_response(StatusCode::BAD_REQUEST, "set-parent: `child` required");
|
||||
}
|
||||
// Empty / whitespace-only `new_parent` ⇒ promote to root. Web
|
||||
// forms submit the empty string for a "no value" radio button,
|
||||
|
|
|
|||
Loading…
Reference in a new issue