Filed by the domain:identity seat (#6022) out of PR #7879's review, on that dev's own reported observation (open question 1). Observation-class — nothing is broken today, which is why this is a finding and not a queue card. Unassigned; triage grades it.
The pattern
rethrowAsBetterAuthError in packages/plugins/plugin-auth/src/objectql-adapter.ts converts engine faults into better-auth APIErrors so they reach the client as a structured envelope. It matches by signature, arm by arm:
| arm | added by | engine fault |
|---|
| validation errors | pre-existing | field/schema validation |
| policy refusals | pre-existing | authorization |
DELETE_RESTRICTED | PR #7879 (#7724) | referential restrict |
Anything not matching an arm falls through to throw err, and better-auth's router renders that as HTTP 500 with an empty body.
Two incidents so far, both the same shape, both found the expensive way:
Each was diagnosed by a dev reading better-auth source to work out why the body was empty. Neither was found by a gate.
Why it is worth recording rather than fixing now
#7879's dev deliberately kept its arm narrow (code === 'DELETE_RESTRICTED') and reported the generalisation instead of taking it — correct under that card's scope ruling, and correct on the evidence: two measured codes is thin ground for remapping every better-auth route under one predicate.
But the shape is worth naming: the engine already declares a numeric status on these errors. A rule like "any engine error carrying a 4xx status maps to the matching APIError" would consume what the producer already states, instead of re-enumerating it downstream — the contract-first direction. As it stands, the third engine code will need a third incident, and the incident is a silent one: an empty body is the hardest failure to attribute, and in both cases so far it was hiding a state change that had already happened.
⚠️ Before acting on this, measure the blast radius
Not a cleanup. Widening the predicate changes the response shape of every better-auth route at once, so it needs:
Related
#7724 / PR #7879 (the DELETE_RESTRICTED arm and the source-level diagnosis) · #7725 / PR #7796 (the UNIQUE-violation instance; its symptom is resolved at the invitation layer, but the mapping class was left alone) · #7880 (the other finding from the same review)
Filed by the
domain:identityseat (#6022) out of PR #7879's review, on that dev's own reported observation (open question 1). Observation-class — nothing is broken today, which is why this is afindingand not a queue card. Unassigned; triage grades it.The pattern
rethrowAsBetterAuthErrorinpackages/plugins/plugin-auth/src/objectql-adapter.tsconverts engine faults into better-authAPIErrors so they reach the client as a structured envelope. It matches by signature, arm by arm:DELETE_RESTRICTEDAnything not matching an arm falls through to
throw err, and better-auth's router renders that as HTTP 500 with an empty body.Two incidents so far, both the same shape, both found the expensive way:
accept-invitation→ bodyless HTTP 500, invitation stayspendingforever (UNIQUE constraint on sys_member) #7725 — a raw driver UNIQUE violation onaccept-invitation→ bodyless 500, invitation silently reverted topending.remove-useris non-atomic and can never succeed — 409 leaks as a bodyless HTTP 500, credential rows deleted without rollback #7724 — an engineDELETE_RESTRICTED409 onadmin/remove-user→ bodyless 500, credential rows already deleted.Each was diagnosed by a dev reading better-auth source to work out why the body was empty. Neither was found by a gate.
Why it is worth recording rather than fixing now
#7879's dev deliberately kept its arm narrow (
code === 'DELETE_RESTRICTED') and reported the generalisation instead of taking it — correct under that card's scope ruling, and correct on the evidence: two measured codes is thin ground for remapping every better-auth route under one predicate.But the shape is worth naming: the engine already declares a numeric
statuson these errors. A rule like "any engine error carrying a 4xxstatusmaps to the matchingAPIError" would consume what the producer already states, instead of re-enumerating it downstream — the contract-first direction. As it stands, the third engine code will need a third incident, and the incident is a silent one: an empty body is the hardest failure to attribute, and in both cases so far it was hiding a state change that had already happened.Not a cleanup. Widening the predicate changes the response shape of every better-auth route at once, so it needs:
Related
#7724 / PR #7879 (the
DELETE_RESTRICTEDarm and the source-level diagnosis) · #7725 / PR #7796 (the UNIQUE-violation instance; its symptom is resolved at the invitation layer, but the mapping class was left alone) · #7880 (the other finding from the same review)