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
+79
-68
@@ -1,3 +1,4 @@
|
||||
use crate::error::ApiError;
|
||||
use actix_web::dev::{ServiceFactory, ServiceRequest};
|
||||
use actix_web::{App, HttpRequest, HttpResponse, Responder, web};
|
||||
use chrono::Utc;
|
||||
@@ -519,7 +520,7 @@ async fn list_entities<D: KnowledgeDao + 'static>(
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("list_entities error: {:?}", e);
|
||||
HttpResponse::InternalServerError().json(serde_json::json!({"error": "Database error"}))
|
||||
ApiError::internal("Database error").into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -538,12 +539,12 @@ async fn get_entity<D: KnowledgeDao + 'static>(
|
||||
let entity = match dao.get_entity_by_id(entity_id) {
|
||||
Ok(Some(e)) => e,
|
||||
Ok(None) => {
|
||||
return HttpResponse::NotFound().json(serde_json::json!({"error": "Entity not found"}));
|
||||
return ApiError::not_found("entity_not_found", "That entity no longer exists")
|
||||
.into_response();
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("get_entity error: {:?}", e);
|
||||
return HttpResponse::InternalServerError()
|
||||
.json(serde_json::json!({"error": "Database error"}));
|
||||
return ApiError::internal("Database error").into_response();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -552,8 +553,7 @@ async fn get_entity<D: KnowledgeDao + 'static>(
|
||||
Ok(f) => f,
|
||||
Err(e) => {
|
||||
log::error!("get_facts_for_entity error: {:?}", e);
|
||||
return HttpResponse::InternalServerError()
|
||||
.json(serde_json::json!({"error": "Database error"}));
|
||||
return ApiError::internal("Database error").into_response();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -644,8 +644,7 @@ async fn get_entity<D: KnowledgeDao + 'static>(
|
||||
Ok(links) => links.into_iter().map(PhotoLinkDetail::from).collect(),
|
||||
Err(e) => {
|
||||
log::error!("get_links_for_entity error: {:?}", e);
|
||||
return HttpResponse::InternalServerError()
|
||||
.json(serde_json::json!({"error": "Database error"}));
|
||||
return ApiError::internal("Database error").into_response();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -693,10 +692,12 @@ async fn patch_entity<D: KnowledgeDao + 'static>(
|
||||
let mut dao = dao.lock().expect("Unable to lock KnowledgeDao");
|
||||
match dao.update_entity(entity_id, patch) {
|
||||
Ok(Some(entity)) => HttpResponse::Ok().json(EntitySummary::from(entity)),
|
||||
Ok(None) => HttpResponse::NotFound().json(serde_json::json!({"error": "Entity not found"})),
|
||||
Ok(None) => {
|
||||
ApiError::not_found("entity_not_found", "That entity no longer exists").into_response()
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("patch_entity error: {:?}", e);
|
||||
HttpResponse::InternalServerError().json(serde_json::json!({"error": "Database error"}))
|
||||
ApiError::internal("Database error").into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -712,12 +713,12 @@ async fn delete_entity<D: KnowledgeDao + 'static>(
|
||||
// Verify entity exists before deleting
|
||||
match dao.get_entity_by_id(entity_id) {
|
||||
Ok(None) => {
|
||||
return HttpResponse::NotFound().json(serde_json::json!({"error": "Entity not found"}));
|
||||
return ApiError::not_found("entity_not_found", "That entity no longer exists")
|
||||
.into_response();
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("delete_entity lookup error: {:?}", e);
|
||||
return HttpResponse::InternalServerError()
|
||||
.json(serde_json::json!({"error": "Database error"}));
|
||||
return ApiError::internal("Database error").into_response();
|
||||
}
|
||||
Ok(Some(_)) => {}
|
||||
}
|
||||
@@ -726,7 +727,7 @@ async fn delete_entity<D: KnowledgeDao + 'static>(
|
||||
Ok(()) => HttpResponse::NoContent().finish(),
|
||||
Err(e) => {
|
||||
log::error!("delete_entity error: {:?}", e);
|
||||
HttpResponse::InternalServerError().json(serde_json::json!({"error": "Database error"}))
|
||||
ApiError::internal("Database error").into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -737,8 +738,8 @@ async fn merge_entities<D: KnowledgeDao + 'static>(
|
||||
dao: web::Data<Mutex<D>>,
|
||||
) -> impl Responder {
|
||||
if body.source_id == body.target_id {
|
||||
return HttpResponse::BadRequest()
|
||||
.json(serde_json::json!({"error": "source_id and target_id must be different"}));
|
||||
return ApiError::bad_request("merge_into_self", "An entity can't be merged into itself")
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let mut dao = dao.lock().expect("Unable to lock KnowledgeDao");
|
||||
@@ -747,13 +748,15 @@ async fn merge_entities<D: KnowledgeDao + 'static>(
|
||||
for id in [body.source_id, body.target_id] {
|
||||
match dao.get_entity_by_id(id) {
|
||||
Ok(None) => {
|
||||
return HttpResponse::BadRequest()
|
||||
.json(serde_json::json!({"error": format!("Entity {} not found", id)}));
|
||||
return ApiError::bad_request(
|
||||
"entity_not_found",
|
||||
format!("Entity {} no longer exists", id),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("merge_entities lookup error: {:?}", e);
|
||||
return HttpResponse::InternalServerError()
|
||||
.json(serde_json::json!({"error": "Database error"}));
|
||||
return ApiError::internal("Database error").into_response();
|
||||
}
|
||||
Ok(Some(_)) => {}
|
||||
}
|
||||
@@ -768,7 +771,7 @@ async fn merge_entities<D: KnowledgeDao + 'static>(
|
||||
}),
|
||||
Err(e) => {
|
||||
log::error!("merge_entities error: {:?}", e);
|
||||
HttpResponse::InternalServerError().json(serde_json::json!({"error": "Database error"}))
|
||||
ApiError::internal("Database error").into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -790,8 +793,8 @@ async fn synthesize_merge<D: KnowledgeDao + 'static>(
|
||||
app_state: web::Data<AppState>,
|
||||
) -> impl Responder {
|
||||
if body.source_id == body.target_id {
|
||||
return HttpResponse::BadRequest()
|
||||
.json(serde_json::json!({"error": "source_id and target_id must differ"}));
|
||||
return ApiError::bad_request("merge_into_self", "An entity can't be merged into itself")
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let (source, target) = {
|
||||
@@ -800,25 +803,29 @@ async fn synthesize_merge<D: KnowledgeDao + 'static>(
|
||||
let source = match dao.get_entity_by_id(body.source_id) {
|
||||
Ok(Some(e)) => e,
|
||||
Ok(None) => {
|
||||
return HttpResponse::BadRequest()
|
||||
.json(serde_json::json!({"error": "source entity not found"}));
|
||||
return ApiError::bad_request(
|
||||
"entity_not_found",
|
||||
"The entity to merge no longer exists",
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("synthesize_merge source lookup: {:?}", e);
|
||||
return HttpResponse::InternalServerError()
|
||||
.json(serde_json::json!({"error": "Database error"}));
|
||||
return ApiError::internal("Database error").into_response();
|
||||
}
|
||||
};
|
||||
let target = match dao.get_entity_by_id(body.target_id) {
|
||||
Ok(Some(e)) => e,
|
||||
Ok(None) => {
|
||||
return HttpResponse::BadRequest()
|
||||
.json(serde_json::json!({"error": "target entity not found"}));
|
||||
return ApiError::bad_request(
|
||||
"entity_not_found",
|
||||
"The entity to merge into no longer exists",
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("synthesize_merge target lookup: {:?}", e);
|
||||
return HttpResponse::InternalServerError()
|
||||
.json(serde_json::json!({"error": "Database error"}));
|
||||
return ApiError::internal("Database error").into_response();
|
||||
}
|
||||
};
|
||||
(source, target)
|
||||
@@ -890,9 +897,11 @@ async fn synthesize_merge<D: KnowledgeDao + 'static>(
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("synthesize_merge generate failed: {:?}", e);
|
||||
return HttpResponse::ServiceUnavailable().json(serde_json::json!({
|
||||
"error": "LLM unavailable; the merge picker should fall back to skip-synthesis."
|
||||
}));
|
||||
return ApiError::unavailable(
|
||||
"llm_unavailable",
|
||||
"The language model is unavailable. Merge without a summary, or try again later.",
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -967,7 +976,7 @@ async fn list_facts<D: KnowledgeDao + 'static>(
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("list_facts error: {:?}", e);
|
||||
HttpResponse::InternalServerError().json(serde_json::json!({"error": "Database error"}))
|
||||
ApiError::internal("Database error").into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -980,13 +989,11 @@ async fn create_fact<D: KnowledgeDao + 'static>(
|
||||
persona_dao: PersonaDaoData,
|
||||
) -> impl Responder {
|
||||
if body.object_entity_id.is_none() && body.object_value.is_none() {
|
||||
return HttpResponse::BadRequest().json(serde_json::json!({
|
||||
"error": "object_entity_id or object_value is required"
|
||||
}));
|
||||
return ApiError::bad_request("invalid_fact", "A fact needs an object entity or a value")
|
||||
.into_response();
|
||||
}
|
||||
if body.predicate.trim().is_empty() {
|
||||
return HttpResponse::BadRequest()
|
||||
.json(serde_json::json!({"error": "predicate must not be empty"}));
|
||||
return ApiError::bad_request("invalid_fact", "A fact needs a predicate").into_response();
|
||||
}
|
||||
|
||||
// Persona scoping: facts are written under the active single persona.
|
||||
@@ -1006,14 +1013,15 @@ async fn create_fact<D: KnowledgeDao + 'static>(
|
||||
// Verify subject entity exists.
|
||||
match dao.get_entity_by_id(body.subject_entity_id) {
|
||||
Ok(None) => {
|
||||
return HttpResponse::BadRequest().json(serde_json::json!({
|
||||
"error": format!("Subject entity {} not found", body.subject_entity_id)
|
||||
}));
|
||||
return ApiError::bad_request(
|
||||
"entity_not_found",
|
||||
"The fact's subject no longer exists",
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("create_fact subject lookup error: {:?}", e);
|
||||
return HttpResponse::InternalServerError()
|
||||
.json(serde_json::json!({"error": "Database error"}));
|
||||
return ApiError::internal("Database error").into_response();
|
||||
}
|
||||
Ok(Some(_)) => {}
|
||||
}
|
||||
@@ -1022,14 +1030,15 @@ async fn create_fact<D: KnowledgeDao + 'static>(
|
||||
if let Some(oid) = body.object_entity_id {
|
||||
match dao.get_entity_by_id(oid) {
|
||||
Ok(None) => {
|
||||
return HttpResponse::BadRequest().json(serde_json::json!({
|
||||
"error": format!("Object entity {} not found", oid)
|
||||
}));
|
||||
return ApiError::bad_request(
|
||||
"entity_not_found",
|
||||
"The fact's object no longer exists",
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("create_fact object lookup error: {:?}", e);
|
||||
return HttpResponse::InternalServerError()
|
||||
.json(serde_json::json!({"error": "Database error"}));
|
||||
return ApiError::internal("Database error").into_response();
|
||||
}
|
||||
Ok(Some(_)) => {}
|
||||
}
|
||||
@@ -1074,7 +1083,7 @@ async fn create_fact<D: KnowledgeDao + 'static>(
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("create_fact upsert error: {:?}", e);
|
||||
HttpResponse::InternalServerError().json(serde_json::json!({"error": "Database error"}))
|
||||
ApiError::internal("Database error").into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1101,10 +1110,12 @@ async fn patch_fact<D: KnowledgeDao + 'static>(
|
||||
// agent corrections.
|
||||
match dao.update_fact(fact_id, patch, Some(("manual", "manual"))) {
|
||||
Ok(Some(fact)) => HttpResponse::Ok().json(fact),
|
||||
Ok(None) => HttpResponse::NotFound().json(serde_json::json!({"error": "Fact not found"})),
|
||||
Ok(None) => {
|
||||
ApiError::not_found("fact_not_found", "That fact no longer exists").into_response()
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("patch_fact error: {:?}", e);
|
||||
HttpResponse::InternalServerError().json(serde_json::json!({"error": "Database error"}))
|
||||
ApiError::internal("Database error").into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1120,7 +1131,7 @@ async fn delete_fact<D: KnowledgeDao + 'static>(
|
||||
Ok(()) => HttpResponse::NoContent().finish(),
|
||||
Err(e) => {
|
||||
log::warn!("delete_fact({}) error: {:?}", fact_id, e);
|
||||
HttpResponse::NotFound().json(serde_json::json!({"error": "Fact not found"}))
|
||||
ApiError::not_found("fact_not_found", "That fact no longer exists").into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1133,8 +1144,8 @@ async fn supersede_fact<D: KnowledgeDao + 'static>(
|
||||
) -> impl Responder {
|
||||
let old_id = id.into_inner();
|
||||
if old_id == body.by_fact_id {
|
||||
return HttpResponse::BadRequest()
|
||||
.json(serde_json::json!({"error": "old_id and by_fact_id must differ"}));
|
||||
return ApiError::bad_request("supersede_self", "A fact can't replace itself")
|
||||
.into_response();
|
||||
}
|
||||
let mut dao = dao.lock().expect("Unable to lock KnowledgeDao");
|
||||
// Manual supersede from the curation UI — same stamping rule as
|
||||
@@ -1142,11 +1153,11 @@ async fn supersede_fact<D: KnowledgeDao + 'static>(
|
||||
match dao.supersede_fact(old_id, body.by_fact_id, Some(("manual", "manual"))) {
|
||||
Ok(Some(fact)) => HttpResponse::Ok().json(fact),
|
||||
Ok(None) => {
|
||||
HttpResponse::NotFound().json(serde_json::json!({"error": "Old or new fact not found"}))
|
||||
ApiError::not_found("fact_not_found", "That fact no longer exists").into_response()
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("supersede_fact error: {:?}", e);
|
||||
HttpResponse::InternalServerError().json(serde_json::json!({"error": "Database error"}))
|
||||
ApiError::internal("Database error").into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1160,10 +1171,12 @@ async fn restore_fact<D: KnowledgeDao + 'static>(
|
||||
let mut dao = dao.lock().expect("Unable to lock KnowledgeDao");
|
||||
match dao.revert_supersession(fact_id, Some(("manual", "manual"))) {
|
||||
Ok(Some(fact)) => HttpResponse::Ok().json(fact),
|
||||
Ok(None) => HttpResponse::NotFound().json(serde_json::json!({"error": "Fact not found"})),
|
||||
Ok(None) => {
|
||||
ApiError::not_found("fact_not_found", "That fact no longer exists").into_response()
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("restore_fact error: {:?}", e);
|
||||
HttpResponse::InternalServerError().json(serde_json::json!({"error": "Database error"}))
|
||||
ApiError::internal("Database error").into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1193,7 +1206,7 @@ async fn get_recent<D: KnowledgeDao + 'static>(
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("get_recent error: {:?}", e);
|
||||
HttpResponse::InternalServerError().json(serde_json::json!({"error": "Database error"}))
|
||||
ApiError::internal("Database error").into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1217,7 +1230,7 @@ async fn get_predicate_stats<D: KnowledgeDao + 'static>(
|
||||
}),
|
||||
Err(e) => {
|
||||
log::error!("get_predicate_stats error: {:?}", e);
|
||||
HttpResponse::InternalServerError().json(serde_json::json!({"error": "Database error"}))
|
||||
ApiError::internal("Database error").into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1231,8 +1244,7 @@ async fn bulk_reject_predicate<D: KnowledgeDao + 'static>(
|
||||
) -> impl Responder {
|
||||
let predicate = predicate.into_inner();
|
||||
if predicate.trim().is_empty() {
|
||||
return HttpResponse::BadRequest()
|
||||
.json(serde_json::json!({"error": "predicate must not be empty"}));
|
||||
return ApiError::bad_request("invalid_fact", "A fact needs a predicate").into_response();
|
||||
}
|
||||
let persona = resolve_persona_filter(&req, &claims, &persona_dao);
|
||||
let mut dao = dao.lock().expect("Unable to lock KnowledgeDao");
|
||||
@@ -1240,7 +1252,7 @@ async fn bulk_reject_predicate<D: KnowledgeDao + 'static>(
|
||||
Ok(rejected) => HttpResponse::Ok().json(BulkRejectResponse { rejected }),
|
||||
Err(e) => {
|
||||
log::error!("bulk_reject_predicate error: {:?}", e);
|
||||
HttpResponse::InternalServerError().json(serde_json::json!({"error": "Database error"}))
|
||||
ApiError::internal("Database error").into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1278,7 +1290,7 @@ async fn get_graph<D: KnowledgeDao + 'static>(
|
||||
}),
|
||||
Err(e) => {
|
||||
log::error!("build_entity_graph error: {:?}", e);
|
||||
HttpResponse::InternalServerError().json(serde_json::json!({"error": "Database error"}))
|
||||
ApiError::internal("Database error").into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1302,8 +1314,7 @@ async fn get_consolidation_proposals<D: KnowledgeDao + 'static>(
|
||||
Ok(g) => g,
|
||||
Err(e) => {
|
||||
log::error!("find_consolidation_proposals: {:?}", e);
|
||||
return HttpResponse::InternalServerError()
|
||||
.json(serde_json::json!({"error": "Database error"}));
|
||||
return ApiError::internal("Database error").into_response();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in new issue
Block a user