Every error response in the { error, code, details? } format (step 2)
Moves every remaining hand-built error response onto ApiError: ~300
sites across faces, image, video, files, memories, versions,
duplicates, favorites, libraries, auth, clip/unified search, prefs,
reels, tts, insight chat, persona chat, knowledge and personas.
- 500s: the body was the error's text (anyhow / diesel / {:?}); now
ApiError::internal logs it and sends a generic message.
- Each 4xx/502/503 has a stable code and a message written for users;
statuses are unchanged. Shared helpers per module (faces_unavailable,
person_not_found, unknown_library, insight_not_found, ...).
- Pre-format top-level fields kept where clients read them: upload 409
duplicate_of / content_hash / library_name.
- PersonaChatError converts into ApiError in one place.
- Multipart upload errors use the format (error::multipart_config).
- Guard test: no non-test code builds HttpResponse::<error>() by hand.
Closes #202
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1836f1b9ca
commit
431c3a7492
23 files changed
+894
-542
No files matched your search
+60
-50
@@ -22,6 +22,7 @@
|
||||
//! registry.
|
||||
//! - The HTTP handlers under ` /persona_chat/*`.
|
||||
|
||||
use crate::error::ApiError;
|
||||
use actix_web::{HttpResponse, Responder, delete, get, post, web};
|
||||
use anyhow::{Context, Result, anyhow, bail};
|
||||
use chrono::Utc;
|
||||
@@ -85,6 +86,39 @@ impl std::fmt::Display for PersonaChatError {
|
||||
|
||||
impl std::error::Error for PersonaChatError {}
|
||||
|
||||
impl From<&PersonaChatError> for ApiError {
|
||||
fn from(e: &PersonaChatError) -> Self {
|
||||
match e {
|
||||
PersonaChatError::UnknownPersona(_) => {
|
||||
ApiError::not_found("persona_not_found", "That persona no longer exists")
|
||||
}
|
||||
PersonaChatError::UnknownConversation(_) => ApiError::not_found(
|
||||
"conversation_not_found",
|
||||
"That conversation no longer exists",
|
||||
),
|
||||
PersonaChatError::EmptyMessage => {
|
||||
ApiError::bad_request("empty_message", "Type a message first")
|
||||
}
|
||||
PersonaChatError::MessageTooLong => ApiError::bad_request(
|
||||
"message_too_long",
|
||||
"That message is too long (8192 characters at most)",
|
||||
),
|
||||
PersonaChatError::ConcurrentTurn => ApiError::conflict(
|
||||
"turn_in_progress",
|
||||
"A reply is still being written. Wait for it to finish.",
|
||||
),
|
||||
PersonaChatError::SmsThread(detail) => {
|
||||
log::warn!("persona chat: SMS thread unreadable: {detail}");
|
||||
ApiError::bad_gateway(
|
||||
"sms_thread_unavailable",
|
||||
"Couldn't read that message thread",
|
||||
)
|
||||
}
|
||||
PersonaChatError::Db(err) => ApiError::internal(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<anyhow::Error> for PersonaChatError {
|
||||
fn from(e: anyhow::Error) -> Self {
|
||||
PersonaChatError::Db(e)
|
||||
@@ -1463,12 +1497,14 @@ pub async fn persona_chat_history_handler(
|
||||
) {
|
||||
Ok(view) => HttpResponse::Ok().json(view),
|
||||
Err(e) => {
|
||||
let msg = format!("{e}");
|
||||
if msg.contains("not found") {
|
||||
HttpResponse::NotFound().json(serde_json::json!({ "error": msg }))
|
||||
if format!("{e}").contains("not found") {
|
||||
ApiError::not_found(
|
||||
"conversation_not_found",
|
||||
"That conversation no longer exists",
|
||||
)
|
||||
.into_response()
|
||||
} else {
|
||||
log::error!("persona chat history load failed: {msg}");
|
||||
HttpResponse::InternalServerError().json(serde_json::json!({ "error": msg }))
|
||||
ApiError::internal(e).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1494,11 +1530,7 @@ pub async fn persona_chat_turn_handler(
|
||||
match libraries::resolve_library_param_state(&app_state, request.library.as_deref()) {
|
||||
Ok(Some(lib)) => lib,
|
||||
Ok(None) => app_state.primary_library(),
|
||||
Err(e) => {
|
||||
return HttpResponse::BadRequest().json(serde_json::json!({
|
||||
"error": format!("invalid library: {e}")
|
||||
}));
|
||||
}
|
||||
Err(e) => return crate::libraries::unknown_library(e).into_response(),
|
||||
};
|
||||
|
||||
let user_id = claims.sub.parse::<i32>().unwrap_or(1);
|
||||
@@ -1519,21 +1551,7 @@ pub async fn persona_chat_turn_handler(
|
||||
}
|
||||
Err(e) => {
|
||||
span.set_status(Status::error(format!("{e}")));
|
||||
match &e {
|
||||
PersonaChatError::UnknownPersona(_) | PersonaChatError::UnknownConversation(_) => {
|
||||
HttpResponse::NotFound()
|
||||
}
|
||||
PersonaChatError::EmptyMessage | PersonaChatError::MessageTooLong => {
|
||||
HttpResponse::BadRequest()
|
||||
}
|
||||
PersonaChatError::ConcurrentTurn => HttpResponse::Conflict(),
|
||||
PersonaChatError::SmsThread(_) => HttpResponse::BadGateway(),
|
||||
PersonaChatError::Db(_) => {
|
||||
log::error!("persona chat dispatch failed: {e}");
|
||||
HttpResponse::InternalServerError()
|
||||
}
|
||||
}
|
||||
.json(serde_json::json!({ "error": format!("{e}") }))
|
||||
ApiError::from(&e).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1567,20 +1585,7 @@ pub async fn persona_chat_create_conversation_handler(
|
||||
Ok(conversation_id) => HttpResponse::Created().json(serde_json::json!({
|
||||
"conversation_id": conversation_id,
|
||||
})),
|
||||
Err(e) => match &e {
|
||||
PersonaChatError::UnknownPersona(_) => {
|
||||
HttpResponse::NotFound().json(serde_json::json!({ "error": format!("{e}") }))
|
||||
}
|
||||
PersonaChatError::SmsThread(_) => {
|
||||
log::warn!("anchored persona chat create failed: {e}");
|
||||
HttpResponse::BadGateway().json(serde_json::json!({ "error": format!("{e}") }))
|
||||
}
|
||||
_ => {
|
||||
log::error!("persona chat create failed: {e}");
|
||||
HttpResponse::InternalServerError()
|
||||
.json(serde_json::json!({ "error": format!("{e}") }))
|
||||
}
|
||||
},
|
||||
Err(e) => ApiError::from(&e).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1600,9 +1605,7 @@ pub async fn persona_chat_delete_conversation_handler(
|
||||
Ok(()) => HttpResponse::Ok().json(serde_json::json!({ "deleted": true })),
|
||||
Err(e) => {
|
||||
log::error!("persona chat delete failed: {e}");
|
||||
HttpResponse::InternalServerError().json(serde_json::json!({
|
||||
"error": format!("{e}")
|
||||
}))
|
||||
ApiError::internal(format!("{e}")).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1626,9 +1629,7 @@ pub async fn persona_chat_conversations_handler(
|
||||
})),
|
||||
Err(e) => {
|
||||
log::error!("persona chat conversation list failed: {e}");
|
||||
HttpResponse::InternalServerError().json(serde_json::json!({
|
||||
"error": format!("{e}")
|
||||
}))
|
||||
ApiError::internal(format!("{e}")).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1669,20 +1670,29 @@ pub struct PersonaChatBranchesQuery {
|
||||
/// equivalents return, so the client can share its error handling.
|
||||
fn branch_error_response(context: &str, e: &anyhow::Error) -> HttpResponse {
|
||||
let msg = format!("{e}");
|
||||
log::error!("{context}: {msg}");
|
||||
log::warn!("{context}: {msg}");
|
||||
if msg.contains("conversation not found") {
|
||||
HttpResponse::NotFound().json(serde_json::json!({ "error": msg }))
|
||||
ApiError::not_found(
|
||||
"conversation_not_found",
|
||||
"That conversation no longer exists",
|
||||
)
|
||||
} else if msg.contains("no chat history") {
|
||||
HttpResponse::Conflict().json(serde_json::json!({ "error": msg }))
|
||||
ApiError::conflict("no_chat_history", "There's no chat here yet")
|
||||
} else if msg.contains("not found")
|
||||
|| msg.contains("not a leaf")
|
||||
|| msg.contains("out of range")
|
||||
|| msg.contains("cannot rewind past")
|
||||
{
|
||||
HttpResponse::BadRequest().json(serde_json::json!({ "error": msg }))
|
||||
// The client's view of the branches is stale.
|
||||
ApiError::bad_request(
|
||||
"stale_branch",
|
||||
"This conversation changed. Reload it and try again.",
|
||||
)
|
||||
.with_details(serde_json::json!({ "reason": msg }))
|
||||
} else {
|
||||
HttpResponse::InternalServerError().json(serde_json::json!({ "error": msg }))
|
||||
ApiError::internal(msg)
|
||||
}
|
||||
.into_response()
|
||||
}
|
||||
|
||||
/// POST /persona_chat/rewind — move the tail of the conversation off the
|
||||
|
||||
Reference in new issue
Block a user