You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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
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.
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.
Found while implementing #14389 (the
classifyDataErrorarm 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-sqlon better-sqlite3), one insert conflict on a single-column unique index, the thrownDuplicateRecordErrorhanded to both REST exits:mapDataError(envelope, 'duly_note')— the exitPOST /api/v1/data/:objectuses — aftermapDataErrorhas noDUPLICATE_RECORDarm: the engine's insert conflict envelope reaches the wire through the generic passthrough, dropping thefieldkey and the user-facing conflict sentence #14389:409 {"error":"A record with this email already exists","code":"UNIQUE_VIOLATION","declaredCode":"DUPLICATE_RECORD","developerMessage":"Duplicate record refused on 'duly_note': a unique constraint on 'email' already holds this value. No record was written.","field":"email","object":"duly_note"}sendThrownError(res, envelope, 'duly_note')— whathandleRouteErroremits — before and aftermapDataErrorhas noDUPLICATE_RECORDarm: the engine's insert conflict envelope reaches the wire through the generic passthrough, dropping thefieldkey and the user-facing conflict sentence #14389, identical:409 {"error":"Duplicate record refused on 'duly_note': a unique constraint on 'email' already holds this value. No record was written.","code":"DUPLICATE_RECORD"}No
field, noobject, the engine's sentence rather than the curated one, andcodeis the engine's spelling rather than the wire'sUNIQUE_VIOLATION.Mechanism
resolveErrorResponse(packages/rest/src/error-response.ts) takes its own declared-status passthrough — thetypeof error.status === 'number' && 400..599read, excluding onlyOBJECT_NOT_FOUND— BEFORE it delegates tomapDataError. The engine's envelope declaresstatus: 409, so on every route that reports throughhandleRouteError/sendThrownErrorthe structured 409 arms inclassifyDataError(DELETE_RESTRICTED,CONCURRENT_UPDATE, and now theDUPLICATE_RECORDarm from #14389) are never reached. The single-record data routes callmapDataErrordirectly 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
handleRouteErrorside (fromrest-server.ts):createMany,updateMany,deleteMany,batch,clone, the import/export routes, and the metadata / UI families. The same mechanism should apply toDELETE_RESTRICTEDon the bulk delete doors — the engine setserr.status = 409on it too, sodependentObject/dependentCount/developerMessagewould be dropped there; not separately measured here, worth one measurement before triage.What is NOT claimed
mapDataErrorhas noDUPLICATE_RECORDarm: the engine's insert conflict envelope reaches the wire through the generic passthrough, dropping thefieldkey and the user-facing conflict sentence #14389 restores the single-record door only; this card is the other door.Shape of a fix (for triage, not decided here)
Consult the bespoke structured-code arms before the
.statuspassthrough inresolveErrorResponse— either by routing a 4xx that carries a code with a bespokeclassifyDataErrorarm throughmapDataError, 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 wantsneeds:contract-review.