Skip to content

Routes exiting through handleRouteError answer the engine's DUPLICATE_RECORD envelope from resolveErrorResponse's .status passthrough — field and object dropped, mapDataError's structured 409 arms never consulted #14541

Description

@os-trump

Found while implementing #14389 (the classifyDataError arm for the engine's insert-conflict envelope). That card's scope fence forbids touching the passthrough, so this is filed rather than ridden on its PR. Related: #14389, #7307, #7525.

Measured

origin/main @ ed44512199, real engine, real drivers (driver-memory, driver-sqlite-wasm, driver-sql on better-sqlite3), one insert conflict on a single-column unique index, the thrown DuplicateRecordError handed to both REST exits:

No field, no object, the engine's sentence rather than the curated one, and code is the engine's spelling rather than the wire's UNIQUE_VIOLATION.

Mechanism

resolveErrorResponse (packages/rest/src/error-response.ts) takes its own declared-status passthrough — the typeof error.status === 'number' && 400..599 read, excluding only OBJECT_NOT_FOUND — BEFORE it delegates to mapDataError. The engine's envelope declares status: 409, so on every route that reports through handleRouteError / sendThrownError the structured 409 arms in classifyDataError (DELETE_RESTRICTED, CONCURRENT_UPDATE, and now the DUPLICATE_RECORD arm from #14389) are never reached. The single-record data routes call mapDataError directly and do reach them. One refusal, two bodies, decided by which route caught it — the door-disagreement class the file already records under #7525 / #8016 / #11588.

Routes on the handleRouteError side (from rest-server.ts): createMany, updateMany, deleteMany, batch, clone, the import/export routes, and the metadata / UI families. The same mechanism should apply to DELETE_RESTRICTED on the bulk delete doors — the engine sets err.status = 409 on it too, so dependentObject / dependentCount / developerMessage would be dropped there; not separately measured here, worth one measurement before triage.

What is NOT claimed

Shape of a fix (for triage, not decided here)

Consult the bespoke structured-code arms before the .status passthrough in resolveErrorResponse — either by routing a 4xx that carries a code with a bespoke classifyDataError arm through mapDataError, or by lifting the structured arms into a shared classification both doors ask first. Either way it changes bodies on published bulk doors, so it is a wire-contract change and likely wants needs:contract-review.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions