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:
Cameron CordesandClaude Opus 5.5 committed 2026-11-01 13:29:13 -05:00
1 parent 1836f1b9ca
commit 431c3a7492
23 files changed
+894 -542

No files matched your search

+23 -11
View File
@@ -3,6 +3,7 @@
//! Both take the path of the original *or* of any version, and answer about
//! the whole set. The display choice is global (not per user).
use crate::error::ApiError;
use std::sync::Mutex;
use actix_web::{HttpResponse, Responder, get, put, web};
@@ -84,14 +85,14 @@ fn resolve(
) -> Result<Resolved, HttpResponse> {
let library_id = match libraries::resolve_library_param_state(app_state, library) {
Ok(lib) => lib.map(|l| l.id),
Err(msg) => return Err(HttpResponse::BadRequest().body(msg)),
Err(msg) => return Err(libraries::unknown_library(msg).into_response()),
};
let path = normalize_path(path);
let mut rows = exif_dao
.lock()
.expect("Unable to lock ExifDao")
.get_exif_batch(library_id, std::slice::from_ref(&path))
.map_err(|_| HttpResponse::InternalServerError().finish())?;
.map_err(|_| ApiError::internal("internal error").into_response())?;
// Without `library`, a path can name rows in several libraries. Take the
// earliest hashed one; it then resolves to its original below, so a
// version in one library and its original in another agree.
@@ -100,14 +101,18 @@ fn resolve(
.iter()
.find_map(|r| r.content_hash.clone().map(|h| (r, h)))
else {
return Err(HttpResponse::NotFound().body("no indexed file at that path"));
return Err(ApiError::not_found(
"photo_not_indexed",
"This photo isn't indexed yet. Try again in a minute.",
)
.into_response());
};
let link = app_state
.version_dao
.lock()
.expect("Unable to lock VersionDao")
.get_link(&hash)
.map_err(|_| HttpResponse::InternalServerError().finish())?;
.map_err(|_| ApiError::internal("internal error").into_response())?;
Ok(Resolved {
original_hash: link.map(|l| l.original_hash).unwrap_or(hash),
library_id: row.library_id,
@@ -196,7 +201,7 @@ pub async fn get_versions(
Ok(body) => HttpResponse::Ok().json(body),
Err(e) => {
warn!("get_versions failed: {:?}", e);
HttpResponse::InternalServerError().finish()
ApiError::internal("internal error").into_response()
}
}
}
@@ -233,11 +238,15 @@ pub async fn put_display(
Ok(r) => r,
Err(e) => {
warn!("put_display lookup failed: {:?}", e);
return HttpResponse::InternalServerError().finish();
return ApiError::internal("internal error").into_response();
}
};
if !current.versions.iter().any(|v| v.hash == hash) {
return HttpResponse::BadRequest().body("display_hash is not a version of this photo");
return ApiError::bad_request(
"not_a_version",
"That file isn't a version of this photo",
)
.into_response();
}
// The display file is fetched through the original's library.
let original_lib = current.original.as_ref().map(|o| o.library_id);
@@ -246,19 +255,22 @@ pub async fn put_display(
.iter()
.any(|v| v.hash == hash && original_lib.is_none_or(|l| l == v.library_id))
{
return HttpResponse::BadRequest()
.body("display_hash has no file in the original's library");
return ApiError::bad_request(
"version_in_other_library",
"That version isn't in the same library as the original",
)
.into_response();
}
}
if let Err(e) = dao.set_display(&resolved.original_hash, wanted) {
warn!("put_display failed: {:?}", e);
return HttpResponse::InternalServerError().finish();
return ApiError::internal("internal error").into_response();
}
match build_response(dao.as_mut(), &resolved.original_hash, resolved.library_id) {
Ok(body) => HttpResponse::Ok().json(body),
Err(e) => {
warn!("put_display response failed: {:?}", e);
HttpResponse::InternalServerError().finish()
ApiError::internal("internal error").into_response()
}
}
}