One error response format: { error, code, details? } (step 1)

- error::ApiError (ResponseError): status + stable snake_case code +
  display-safe message + optional details. From<anyhow>/From<diesel> and
  into_http_internal_err become a 500 "internal" whose cause is logged,
  never sent.
- JSON body, query and path parse failures, the Claims 401 and unknown
  routes now answer in the format instead of actix's plain text.
- tags handlers return ApiResult with codes (invalid_tag_name,
  tag_not_found, tag_name_taken, no_files, too_many_files,
  nothing_to_change, tag_added_and_removed). The rename 409 keeps
  existing_tag at the top level for older clients.
- Enhance's error_response builds through ApiError; its codes are
  unchanged, and an internal error no longer sends its message.
- duplicates: eight 500s sent format!("{:?}", e) to the client.

Refs #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:03:06 -05:00
1 parent 96a2858a50
commit 13c244765f
7 files changed
+470 -146

No files matched your search

+19 -12
View File
@@ -24,27 +24,34 @@ use super::service::{EnhanceError, EnhanceService, SourceFile};
use crate::data::Claims;
use crate::database::ExifDao;
use crate::database::enhance_dao::{EnhanceJobRow, STATUS_APPROVED, STATUS_READY};
use crate::error::ApiError;
use crate::libraries::{self, Library};
use crate::state::AppState;
use crate::utils::normalize_path;
use crate::versions::handlers::build_response;
use actix_web::ResponseError;
type ExifData = web::Data<Mutex<Box<dyn ExifDao>>>;
/// Enhance errors in the shared format (#202). The codes predate it and stay
/// as they are: clients may already branch on them.
fn error_response(e: &EnhanceError) -> HttpResponse {
let (mut builder, code) = match e {
EnhanceError::Disabled => (HttpResponse::ServiceUnavailable(), "disabled"),
EnhanceError::Unavailable => (HttpResponse::ServiceUnavailable(), "unavailable"),
EnhanceError::UnknownTemplate(_) => (HttpResponse::NotFound(), "unknown_template"),
EnhanceError::BadRequest(_) => (HttpResponse::BadRequest(), "bad_request"),
EnhanceError::Rejected(_) => (HttpResponse::UnprocessableEntity(), "rejected"),
EnhanceError::Expired => (HttpResponse::Gone(), "expired"),
EnhanceError::NotFound => (HttpResponse::NotFound(), "not_found"),
EnhanceError::Unplaceable(_) => (HttpResponse::BadGateway(), "unplaceable"),
EnhanceError::Upstream(_) => (HttpResponse::BadGateway(), "upstream"),
EnhanceError::Internal(_) => (HttpResponse::InternalServerError(), "internal"),
use actix_web::http::StatusCode;
let (status, code) = match e {
EnhanceError::Disabled => (StatusCode::SERVICE_UNAVAILABLE, "disabled"),
EnhanceError::Unavailable => (StatusCode::SERVICE_UNAVAILABLE, "unavailable"),
EnhanceError::UnknownTemplate(_) => (StatusCode::NOT_FOUND, "unknown_template"),
EnhanceError::BadRequest(_) => (StatusCode::BAD_REQUEST, "bad_request"),
EnhanceError::Rejected(_) => (StatusCode::UNPROCESSABLE_ENTITY, "rejected"),
EnhanceError::Expired => (StatusCode::GONE, "expired"),
EnhanceError::NotFound => (StatusCode::NOT_FOUND, "not_found"),
EnhanceError::Unplaceable(_) => (StatusCode::BAD_GATEWAY, "unplaceable"),
EnhanceError::Upstream(_) => (StatusCode::BAD_GATEWAY, "upstream"),
EnhanceError::Internal(_) => {
return ApiError::internal(e).error_response();
}
};
builder.json(json!({ "error": e.message(), "code": code }))
ApiError::new(status, code, e.message()).error_response()
}
fn bad(msg: &str) -> HttpResponse {