fix(#2175): return 409/400 not 500 for turn-in-flight + validation errors
This commit is contained in:
parent
cea2f2faff
commit
c708f698ee
1 changed files with 54 additions and 18 deletions
|
|
@ -531,11 +531,29 @@ async fn api_loose_ends(State(state): State<AppState>) -> Response {
|
||||||
{
|
{
|
||||||
Ok(Ok(hive_sh4re::Response::LooseEnds { loose_ends })) => loose_ends,
|
Ok(Ok(hive_sh4re::Response::LooseEnds { loose_ends })) => loose_ends,
|
||||||
Ok(Ok(hive_sh4re::Response::Err { message })) => {
|
Ok(Ok(hive_sh4re::Response::Err { message })) => {
|
||||||
return error_response(&format!("get_loose_ends: {message}"));
|
return error_response(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
&format!("get_loose_ends: {message}"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(Ok(other)) => {
|
||||||
|
return error_response(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
&format!("unexpected response: {other:?}"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(Err(e)) => {
|
||||||
|
return error_response(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
&format!("transport: {e:#}"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
return error_response(
|
||||||
|
StatusCode::CONFLICT,
|
||||||
|
"get_loose_ends: timed out — hive-c0re busy, retry",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
Ok(Ok(other)) => return error_response(&format!("unexpected response: {other:?}")),
|
|
||||||
Ok(Err(e)) => return error_response(&format!("transport: {e:#}")),
|
|
||||||
Err(_) => return error_response("get_loose_ends: timed out — hive-c0re busy, retry"),
|
|
||||||
};
|
};
|
||||||
axum::Json(serde_json::json!({ "loose_ends": loose_ends })).into_response()
|
axum::Json(serde_json::json!({ "loose_ends": loose_ends })).into_response()
|
||||||
}
|
}
|
||||||
|
|
@ -877,7 +895,7 @@ struct SendForm {
|
||||||
async fn post_send(State(state): State<AppState>, Form(form): Form<SendForm>) -> Response {
|
async fn post_send(State(state): State<AppState>, Form(form): Form<SendForm>) -> Response {
|
||||||
let body = form.body.trim().to_owned();
|
let body = form.body.trim().to_owned();
|
||||||
if body.is_empty() {
|
if body.is_empty() {
|
||||||
return error_response("send: `body` required");
|
return error_response(StatusCode::BAD_REQUEST, "send: `body` required");
|
||||||
}
|
}
|
||||||
let result = match tokio::time::timeout(
|
let result = match tokio::time::timeout(
|
||||||
SOCKET_FETCH_TIMEOUT,
|
SOCKET_FETCH_TIMEOUT,
|
||||||
|
|
@ -902,7 +920,10 @@ async fn post_send(State(state): State<AppState>, Form(form): Form<SendForm>) ->
|
||||||
// inbox row gets consumed by the time `TurnEnd` fires the
|
// inbox row gets consumed by the time `TurnEnd` fires the
|
||||||
// existing turn-end refresh.
|
// existing turn-end refresh.
|
||||||
Ok(()) => (axum::http::StatusCode::OK, "ok").into_response(),
|
Ok(()) => (axum::http::StatusCode::OK, "ok").into_response(),
|
||||||
Err(e) => error_response(&format!("send failed: {e}")),
|
Err(e) => error_response(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
&format!("send failed: {e}"),
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -984,7 +1005,10 @@ async fn post_login_start(State(state): State<AppState>) -> Response {
|
||||||
state.bus.emit_status("needs_login_in_progress");
|
state.bus.emit_status("needs_login_in_progress");
|
||||||
(axum::http::StatusCode::OK, "ok").into_response()
|
(axum::http::StatusCode::OK, "ok").into_response()
|
||||||
}
|
}
|
||||||
Err(e) => error_response(&format!("login start failed: {e:#}")),
|
Err(e) => error_response(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
&format!("login start failed: {e:#}"),
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -996,10 +1020,13 @@ struct CodeForm {
|
||||||
async fn post_login_code(State(state): State<AppState>, Form(form): Form<CodeForm>) -> Response {
|
async fn post_login_code(State(state): State<AppState>, Form(form): Form<CodeForm>) -> Response {
|
||||||
let session = state.session.lock().unwrap().clone();
|
let session = state.session.lock().unwrap().clone();
|
||||||
let Some(session) = session else {
|
let Some(session) = session else {
|
||||||
return error_response("no login session running");
|
return error_response(StatusCode::CONFLICT, "no login session running");
|
||||||
};
|
};
|
||||||
if let Err(e) = session.submit_code(&form.code).await {
|
if let Err(e) = session.submit_code(&form.code).await {
|
||||||
return error_response(&format!("submit code failed: {e:#}"));
|
return error_response(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
&format!("submit code failed: {e:#}"),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
(axum::http::StatusCode::OK, "ok").into_response()
|
(axum::http::StatusCode::OK, "ok").into_response()
|
||||||
}
|
}
|
||||||
|
|
@ -1035,7 +1062,7 @@ struct ModelForm {
|
||||||
async fn post_set_model(State(state): State<AppState>, Form(form): Form<ModelForm>) -> Response {
|
async fn post_set_model(State(state): State<AppState>, Form(form): Form<ModelForm>) -> Response {
|
||||||
let name = form.model.trim();
|
let name = form.model.trim();
|
||||||
if name.is_empty() {
|
if name.is_empty() {
|
||||||
return error_response("model: name required");
|
return error_response(StatusCode::BAD_REQUEST, "model: name required");
|
||||||
}
|
}
|
||||||
state.bus.set_model(name);
|
state.bus.set_model(name);
|
||||||
state.bus.emit(crate::events::LiveEvent::Note {
|
state.bus.emit(crate::events::LiveEvent::Note {
|
||||||
|
|
@ -1059,10 +1086,13 @@ struct EffortForm {
|
||||||
async fn post_set_effort(State(state): State<AppState>, Form(form): Form<EffortForm>) -> Response {
|
async fn post_set_effort(State(state): State<AppState>, Form(form): Form<EffortForm>) -> Response {
|
||||||
let level = form.effort.trim();
|
let level = form.effort.trim();
|
||||||
if !crate::events::is_valid_effort(level) {
|
if !crate::events::is_valid_effort(level) {
|
||||||
return error_response(&format!(
|
return error_response(
|
||||||
"effort: level must be one of {}",
|
StatusCode::BAD_REQUEST,
|
||||||
crate::events::EFFORT_LEVELS.join(", ")
|
&format!(
|
||||||
));
|
"effort: level must be one of {}",
|
||||||
|
crate::events::EFFORT_LEVELS.join(", ")
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
state.bus.set_effort(level);
|
state.bus.set_effort(level);
|
||||||
state.bus.emit(crate::events::LiveEvent::Note {
|
state.bus.emit(crate::events::LiveEvent::Note {
|
||||||
|
|
@ -1079,7 +1109,10 @@ async fn post_compact(State(state): State<AppState>) -> Response {
|
||||||
// Reject immediately if a normal turn is in flight — concurrent access
|
// Reject immediately if a normal turn is in flight — concurrent access
|
||||||
// to the claude session is unsafe and produces garbled output.
|
// to the claude session is unsafe and produces garbled output.
|
||||||
let Ok(guard) = lock.try_lock_owned() else {
|
let Ok(guard) = lock.try_lock_owned() else {
|
||||||
return error_response("turn in flight — wait for it to finish before compacting");
|
return error_response(
|
||||||
|
StatusCode::CONFLICT,
|
||||||
|
"turn in flight — wait for it to finish before compacting",
|
||||||
|
);
|
||||||
};
|
};
|
||||||
let bus = state.bus.clone();
|
let bus = state.bus.clone();
|
||||||
let files = state.files.clone();
|
let files = state.files.clone();
|
||||||
|
|
@ -1211,10 +1244,13 @@ async fn post_cancel_turn(State(state): State<AppState>) -> Response {
|
||||||
(axum::http::StatusCode::OK, "ok").into_response()
|
(axum::http::StatusCode::OK, "ok").into_response()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn error_response(message: &str) -> Response {
|
fn error_response(status: StatusCode, message: &str) -> Response {
|
||||||
// Plain text — JS app surfaces in `alert()`, HTML wrapping would just
|
// Plain text — JS app surfaces in `alert()`, HTML wrapping would just
|
||||||
// be noise.
|
// be noise. Status is per-caller: 400 for bad input, 409 for a
|
||||||
(StatusCode::INTERNAL_SERVER_ERROR, message.to_owned()).into_response()
|
// retryable state conflict (turn in flight / hive-c0re busy), 500 only
|
||||||
|
// for a genuine server/transport failure — the frontend shows the code
|
||||||
|
// in its alert, so a benign "busy, retry" must not read as a 500.
|
||||||
|
(status, message.to_owned()).into_response()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read `HIVE_AVAILABLE_MODELS` (comma-separated short names injected by
|
/// Read `HIVE_AVAILABLE_MODELS` (comma-separated short names injected by
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue