Adopt FastAPI native error detail shape end to end #75
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Severity: HIGH
The bug
The documented
{error: {code, message, details}}envelope never reaches the client.FastAPI serializes
HTTPExceptionas{"detail": <detail>}. The 409 handler(
backend/app/api/routes/photos.py:75-86) raisesdetail={"error": {...}}, so the wire formatis
{"detail": {"error": {...}}}. Every other raise uses a bare string, giving{"detail": "Photo not found"}. Meanwhilefrontend/src/api/client.ts:32-46parses{"error": ...}exclusively.Result: every error code and message is lost. Errors surface as code
"unknown", message"HTTP <status>". And the 409 conflict banner — the flagship concurrency UX — renders"Current version is -1", because
details.current_versionnever arrives.Decision
Adopt FastAPI's native
detailshape rather than keeping the custom envelope. Structureddetail is still available where needed —
detailmay be an object:Scope
{"detail": ...}— a string for simple errors, an object withcodeand anyextra fields for errors the client must act on.
client.tsto parsedetail, handling both the string and object forms.RequestValidationErrorhandler so 422s follow the same shape.docs/circa-api-spec.md, which currently documents the{error}envelope.current_versionfrom the new shape.Note the success envelope
{data, meta}is unaffected and stays as it is.Why this matters beyond the banner
#8's API contract tests are meaningless until the wire format is settled, and this shape is what
the generated TypeScript types (companion issue) will encode.
Done when
detail, with structured content where the client needs itdocs/circa-api-spec.mdmatches realityReferences
backend/app/api/routes/photos.py:75-86frontend/src/api/client.ts:32-46frontend/src/pages/ReviewWorkspacePage.tsx:478-492docs/circa-api-spec.mdDone in
2961bc5. All five "done when" items covered — 16 backend tests intest_error_shape.py, 6 frontend tests inclient.test.ts.Took the decision as written: FastAPI's native
detail, string form for display-only errors, object form withcodefor anything the client branches on. Extra fields sit besidecoderather than under a nesteddetails— one less level to walk, and the nesting is exactly what got lost.Two things worth recording beyond the issue:
current_versioncan benull, and that forced a real fix. When the database-level guard from #77 rejects a write (rather than the pre-check), the server no longer holds a trustworthy read of the row — inventing a number would be worse than admitting we lack one. But the banner usedconflictVersion !== nullas its visibility condition, so passing null would have hidden the banner entirely. Conflict state is now{version: number | null} | null: the outer null means "no conflict", the inner means "we can't name it" and renders "Someone else saved first." Collapsing those two is how "-1" came to be rendered as a version in the first place, so it seemed worth not repeating the shape of the original mistake.422s no longer echo the caller's payload. FastAPI's default includes
inputin each error, which for a rejected login or an oversized note means putting the submitted value back on the wire. Onlylocandmsgcross now, with a test asserting a submitted secret doesn't come back.I dropped the
latest_projectionsnapshot the spec described in the conflict body. The client reloads the photo anyway, and embedding a second representation of a photo in an error is two things that can disagree. Documented that reasoning in the spec rather than silently omitting it.docs/circa-api-spec.mdwas wrong in two places and now matches reality, with a note saying why it changed.This unblocks #8 (contract tests have a settled format to assert against) and #76 (the generated types encode this shape).