Found live on the cloud control plane (cloud.objectos.ai, Postgres/Neon via driver-sql) while auditing browser console errors. Filed here per contract-first: the mechanism is the driver's, every PG-backed deployment inherits it.
Reproduction matrix — one PATCH, only value changes
sys_user_preference.value is a JSON-typed field. PATCH /api/v1/data/sys_user_preference/:id with:
body value | result |
|---|
{"a":1} (object) | 200 |
42 (number) | 200 |
{"items":[1,2]} (array nested in object) | 200 |
[{"type":"app",…}] (non-empty array) | 500 DATABASE_ERROR |
"x" (string) | 500 DATABASE_ERROR |
[] (empty array) | 200 — but silently stored as {} (see mechanism) |
POST behaves identically (a create with an array value 500s).
Mechanism (inferred from the signature — verify at the driver)
The matrix is the classic node-postgres fingerprint for passing JSON-field values to the driver without JSON.stringify:
- JS object → node-pg serialises to JSON text → valid jsonb ✓
- JS array → node-pg converts to a Postgres array literal (
{…}) → invalid JSON text → 500; except [], whose literal {} happens to be valid JSON — so an empty array is accepted and silently stored as an empty object. That last row is data corruption, not an error, and it will outlive any fix that only handles the 500s. - JS string
x → passed raw → not valid JSON text ("x" would be) → 500; a numeric string/number passes because 42 is valid JSON.
Expected: the SQL driver's PG path must JSON.stringify every JSON-field binding (insert AND update), exactly as it presumably does for objects via node-pg's default object handling — relying on node-pg's per-type defaults is the bug.
User-visible impact, today, on every control-plane deployment
The Console's "recent items" preference (ui.recent) is naturally a list. Every navigation writes it → every write 500s → a red DATABASE_ERROR in the browser console on every console visit, recents never persist, and the server logs a 5xx per page view. Both prod and staging control planes are PG, so this is not an environment quirk. (Tenant environments on Turso/SQLite take a different driver path, which is presumably why the seed/data suites never caught it.)
Note for the fix
- The 500 half needs the stringify; the
[]→{} half needs a regression test of its own — it is the case that stays wrong silently after the crash is fixed. - Scalar JSON values (
"x", true, null) are legal JSON documents and should round-trip; a fix that only handles arrays re-fails the string row of the matrix.
Found live on the cloud control plane (
cloud.objectos.ai, Postgres/Neon via driver-sql) while auditing browser console errors. Filed here per contract-first: the mechanism is the driver's, every PG-backed deployment inherits it.Reproduction matrix — one PATCH, only
valuechangessys_user_preference.valueis a JSON-typed field.PATCH /api/v1/data/sys_user_preference/:idwith:value{"a":1}(object)42(number){"items":[1,2]}(array nested in object)[{"type":"app",…}](non-empty array)DATABASE_ERROR"x"(string)DATABASE_ERROR[](empty array){}(see mechanism)POST behaves identically (a create with an array
value500s).Mechanism (inferred from the signature — verify at the driver)
The matrix is the classic node-postgres fingerprint for passing JSON-field values to the driver without
JSON.stringify:{…}) → invalid JSON text → 500; except[], whose literal{}happens to be valid JSON — so an empty array is accepted and silently stored as an empty object. That last row is data corruption, not an error, and it will outlive any fix that only handles the 500s.x→ passed raw → not valid JSON text ("x"would be) → 500; a numeric string/number passes because42is valid JSON.Expected: the SQL driver's PG path must
JSON.stringifyevery JSON-field binding (insert AND update), exactly as it presumably does for objects via node-pg's default object handling — relying on node-pg's per-type defaults is the bug.User-visible impact, today, on every control-plane deployment
The Console's "recent items" preference (
ui.recent) is naturally a list. Every navigation writes it → every write 500s → a redDATABASE_ERRORin the browser console on every console visit, recents never persist, and the server logs a 5xx per page view. Both prod and staging control planes are PG, so this is not an environment quirk. (Tenant environments on Turso/SQLite take a different driver path, which is presumably why the seed/data suites never caught it.)Note for the fix
[]→{}half needs a regression test of its own — it is the case that stays wrong silently after the crash is fixed."x",true,null) are legal JSON documents and should round-trip; a fix that only handles arrays re-fails the string row of the matrix.