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
+20
-18
@@ -27,6 +27,7 @@ use crate::clip_search::{
|
||||
};
|
||||
use crate::data::{Claims, FilterMode, MediaType};
|
||||
use crate::database::ExifDao;
|
||||
use crate::error::ApiError;
|
||||
use crate::file_types::{is_image_file, is_video_file};
|
||||
use crate::geo::{forward_geocode, gps_bounding_box, haversine_distance};
|
||||
use crate::tags::TagDao;
|
||||
@@ -264,14 +265,7 @@ struct UnifiedResponse {
|
||||
results: Vec<SearchHit>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct ErrorBody {
|
||||
error: String,
|
||||
}
|
||||
|
||||
fn bad_request(msg: impl Into<String>) -> HttpResponse {
|
||||
HttpResponse::BadRequest().json(ErrorBody { error: msg.into() })
|
||||
}
|
||||
// Errors use the shared format (crate::error::ApiError, #202).
|
||||
|
||||
/// Combine the model's semantic term with any tag words that didn't match the
|
||||
/// vocab, so a hallucinated/non-vocab tag becomes a soft semantic signal
|
||||
@@ -306,7 +300,7 @@ pub async fn unified_search<TagD: TagDao>(
|
||||
);
|
||||
let nl = query.q.trim().to_string();
|
||||
if nl.is_empty() {
|
||||
return bad_request("query parameter `q` is required");
|
||||
return crate::clip_search::empty_query().into_response();
|
||||
}
|
||||
|
||||
let limit = query.limit.clamp(1, 200);
|
||||
@@ -315,7 +309,7 @@ pub async fn unified_search<TagD: TagDao>(
|
||||
|
||||
let library_ids = match parse_library_scope(query.library_ids.as_deref(), query.library) {
|
||||
Ok(ids) => ids,
|
||||
Err(msg) => return bad_request(msg),
|
||||
Err(msg) => return crate::libraries::unknown_library(msg).into_response(),
|
||||
};
|
||||
|
||||
// ── 1. Translate the NL query, grounded on the real tag vocabulary ──
|
||||
@@ -333,7 +327,13 @@ pub async fn unified_search<TagD: TagDao>(
|
||||
};
|
||||
let (sq, echo, explicit_tags, explicit_gps) = match merge_explicit(sq, &query) {
|
||||
Ok(merged) => merged,
|
||||
Err(msg) => return bad_request(msg),
|
||||
Err(_) => {
|
||||
return ApiError::bad_request(
|
||||
"incomplete_gps_filter",
|
||||
"A location filter needs a latitude, longitude and radius",
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
let folder = folder_prefix(query.path.as_deref());
|
||||
|
||||
@@ -597,7 +597,8 @@ pub async fn unified_search<TagD: TagDao>(
|
||||
// Filters-only: no semantic term. Require at least one filter,
|
||||
// then return the candidate set newest-first.
|
||||
if !has_struct {
|
||||
return bad_request("query had no searchable terms");
|
||||
return ApiError::bad_request("nothing_to_search", "Add search words or a filter")
|
||||
.into_response();
|
||||
}
|
||||
candidate.sort_by(|a, b| b.date_taken.cmp(&a.date_taken));
|
||||
let total_matching = candidate.len();
|
||||
@@ -684,9 +685,11 @@ async fn translate<TagD: TagDao>(
|
||||
Ok(b) => b,
|
||||
Err(e) => {
|
||||
log::warn!("unified_search: resolve_backend failed: {e:?}");
|
||||
return Err(HttpResponse::ServiceUnavailable().json(ErrorBody {
|
||||
error: "LLM backend unavailable".into(),
|
||||
}));
|
||||
return Err(ApiError::unavailable(
|
||||
"llm_unavailable",
|
||||
"The language model is unavailable. Try again shortly.",
|
||||
)
|
||||
.into_response());
|
||||
}
|
||||
};
|
||||
log::info!("unified_search: translating with model={}", backend.model());
|
||||
@@ -700,9 +703,8 @@ async fn translate<TagD: TagDao>(
|
||||
.await
|
||||
.map_err(|e| {
|
||||
log::warn!("unified_search: translate_nl_query failed: {e:?}");
|
||||
HttpResponse::BadGateway().json(ErrorBody {
|
||||
error: "could not interpret the query".into(),
|
||||
})
|
||||
ApiError::bad_gateway("query_not_understood", "Couldn't understand that search")
|
||||
.into_response()
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user