One error response format: { error, code, details? } #202

Closed
opened 2026-11-01 17:53:12 +00:00 by cameron · 0 comments
Owner

Error responses come in five shapes today, so clients can't show useful messages or branch reliably:

Shape Sites
JSON {"error": msg} 144 (+ the global JSON-body handler)
JSON {"error", "code"} Enhance only
Plain text body 78 (faces 27, handlers/image 19, duplicates 11, files, libraries, versions, video, memories)
Plain text from into_http_internal_err 11 (anyhow error text)
Empty body ~95 (.finish() / bare builder), incl. login 401

Plus actix defaults: the Claims extractor's 401 is text "Bad token"; query/path parse failures are actix's text 400.

10 sites send format!("{:?}", e) to the client (mostly duplicates.rs), which can include SQL and library paths.

Contract

{ "error": "Tag name must not be empty", "code": "invalid_tag_name", "details": { } }
  • error: English, safe to show a user as is. The server knows best what went wrong; clients may translate known codes into their own wording.
  • code: stable snake_case id for clients to branch on instead of the status alone.
  • details (optional): structured extras. Existing top-level extras (existing_tag on tag rename 409) stay during migration for compatibility.
  • Unexpected errors: 500, code: "internal", a generic message. The detail is logged, never sent.

Plan

  1. ApiError type (ResponseError) + From<anyhow::Error>; JSON/query/path extractor configs, Claims 401, default 404 for unknown routes all emit the contract; replace into_http_internal_err; move tags and enhance onto it; fix the {:?} leaks. Tests pin the shape.
  2. Migrate the remaining modules, plain-text ones first: faces, handlers/image, duplicates, then the rest.

Client side: FileViewer-React issue (ApiError reads code + message).

Error responses come in five shapes today, so clients can't show useful messages or branch reliably: | Shape | Sites | |---|---| | JSON `{"error": msg}` | 144 (+ the global JSON-body handler) | | JSON `{"error", "code"}` | Enhance only | | Plain text body | 78 (faces 27, handlers/image 19, duplicates 11, files, libraries, versions, video, memories) | | Plain text from `into_http_internal_err` | 11 (anyhow error text) | | Empty body | ~95 (`.finish()` / bare builder), incl. login 401 | Plus actix defaults: the `Claims` extractor's 401 is text "Bad token"; query/path parse failures are actix's text 400. **10 sites send `format!("{:?}", e)` to the client** (mostly `duplicates.rs`), which can include SQL and library paths. ## Contract ```json { "error": "Tag name must not be empty", "code": "invalid_tag_name", "details": { } } ``` - `error`: English, safe to show a user as is. The server knows best what went wrong; clients may translate known codes into their own wording. - `code`: stable snake_case id for clients to branch on instead of the status alone. - `details` (optional): structured extras. Existing top-level extras (`existing_tag` on tag rename 409) stay during migration for compatibility. - Unexpected errors: 500, `code: "internal"`, a generic message. The detail is logged, never sent. ## Plan 1. `ApiError` type (`ResponseError`) + `From<anyhow::Error>`; JSON/query/path extractor configs, `Claims` 401, default 404 for unknown routes all emit the contract; replace `into_http_internal_err`; move `tags` and `enhance` onto it; fix the `{:?}` leaks. Tests pin the shape. 2. Migrate the remaining modules, plain-text ones first: `faces`, `handlers/image`, `duplicates`, then the rest. Client side: FileViewer-React issue (ApiError reads `code` + message).
Sign in to join this conversation.