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
+23
-11
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user