Skip to content

REST: the UNIQUE_VIOLATION 409 message is hard-coded English and carries no field — and it is less informative than the bulk path's own message for the same constraint #7821

Description

@baozhoutao

Version:@objectstack/*@17.0.0-rc.6 (message originates in @objectstack/rest, packages/rest)

What happens

Save a record that violates a unique: true field and the REST layer answers with a hard-coded English sentence that no application can localize or override:

POST /api/v1/data/<object> → 409 Conflict
{"error":"A record with this value already exists","code":"UNIQUE_VIOLATION","object":"<object>"}

Console renders body.error verbatim — in the form's error banner and in the toast. So an application whose every label, option and validation message is fully translated still shows this one English sentence to its end users, on one of the most commonly hit validation paths there is.

This is a server-side string, not a Console one — I checked both directions before filing:

  • the response body above carries it (read off the browser network panel on a real save, not inferred);
  • it appears in none of the Console bundle chunks (index-*.js, framework-*.js, ui-components-*.js, vendor-*.js — 0 hits, including the 2.8 MB ui-components chunk).

Minimal repro

  1. Any object with a text field declared unique: true — e.g. invoice.serial_no.
  2. Create a record with serial_no = "A-001".
  3. Create a second record with the same serial_no, from the Console form or by POST /api/v1/data/invoice.
  4. → 409 with the body above; the Console banner/toast show the raw English sentence.

The part that looks like an outright inconsistency

The same package already knows how to do better on the bulk / row path. sanitizeRowError resolves the offending column and names it:

varUNNAMED_CONFLICT="A record with this value already exists.";functionsanitizeRowError(raw){
...
if(isUniqueViolationError(msg)){constcolumn=uniqueViolationColumn(msg);returncolumn ? `A record with this ${column} already exists.` : UNNAMED_CONFLICT;}

…while the single-record error envelope hard-codes the unnamed variant even though uniqueViolationColumn is right there and the error object is in hand:

if(isUniqueViolationError2(error)){return{status: 409,body: {error: "A record with this value already exists",code: "UNIQUE_VIOLATION",
...object ? { object } : {}}};}

So the same platform gives two different answers to the same constraint depending on whether the write came in one row at a time or in a batch: the batch path tells the user which field collided, the single-record path does not. On a record with several unique fields, the user is told only that "a value" is taken and has to guess which one.

Why a client cannot work around it

code: "UNIQUE_VIOLATION" is in the body, which is enough to detect the class of failure — but the body carries no field / column. A client that wanted to render its own localized message therefore cannot name the field either; the only thing it can produce is a generic "some unique value is taken", which is strictly worse than what the platform itself already computes on the bulk path.

Note the asymmetry with application-authored errors: an error thrown from an app's own hook has its message surfaced verbatim, so app authors can localize those. It is specifically the platform's built-in constraint errors that are stuck in English.

Expected

Either of these would close it; (b) alone is already enough to unblock clients:

  • (a) resolve the message through the i18n service the way other user-facing platform strings are, so a locale bundle can translate it and an application can override it;
  • (b) include the offending field in the 409 body (field / column, alongside the existing code and object), so a client can build its own localized message — and, while there, reuse uniqueViolationColumn in the single-record branch so its default message reaches parity with the bulk path.

Same treatment would apply to the sibling constants in sanitizeRowError ("... is required.", "The database rejected this row (a value may be invalid or already in use).", "Row failed"), which are user-visible on the import path for the same reason.

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions