Repository navigation
A database NOT NULL refusal answers differently per door: the dispatcher /data twin answers 500 INTERNAL_ERROR, and REST batch/import/form/action rows name the column key (RestServer POST/PATCH /data answer 400 with the label after #24715) #24768
Description
Filing gate ① a product defect with a named locus and a measured reproduction. Filed by domain:cli seat 1 (seat post #6024) · zhuangjianguo · session session_01SYEJqDL3NXxrJRBVqaMu9T, from #24715's dev report (PR #24759, open_questions[0] and out_of_scope_findings). ⛔ Not a claim. Domain, grade and route are triage's.
Reader (who acts): triage routes it. The candidate fix sites are in two lanes, packages/objectql (domain:engine) and packages/runtime (domain:cli), so the route is the choice.
What happens
A write that a database NOT NULL constraint refuses gets a different answer depending on the door.
| door | answer for the same refused write (a field declared storage: { notNull: true }, no required, value null) |
|---|---|
RestServer POST /data/:object, PATCH /data/:object/:id |
400 VALIDATION_FAILED, fields[0].code: required. After PR #24759 it names the field by its label, in the caller's locale |
the dispatcher twin (packages/runtime HttpDispatcher → domains/data.ts, rendered by @objectstack/hono's catch-all or dispatcher-plugin's errorResponseBase) |
500 INTERNAL_ERROR, "Internal server error" |
RestServer /data/:object/batch, /batch rows, import rows, the public form submit, data actions |
mapDataError's NOT NULL sentence `${column} is required`: the column key, not the label. Not measured with a labelled field |
Measured by the dev at origin/main 7e7e605de, on a real store (@objectstack/driver-sql + better-sqlite3, ObjectQL, ObjectStackProtocolImplementation):
HttpDispatcher.dispatch('PATCH', '/data/OBJECT/ID', { unit_price: null }), andPOST /data/OBJECTwithout the field, let the rawSqliteError(SQLITE_CONSTRAINT_NOTNULL) out ofdispatch().declaredEnvelopeForThrowdeclines it,resolveThrownHttpErroranswers500 INTERNAL_ERROR, andlooksLikeInternalErrorLeakis true. So both exits answer500 { code: INTERNAL_ERROR, message: 'Internal server error' }.RestServeranswers400 VALIDATION_FAILEDfor the same write.- The reading went through
dispatch()plus the exits' own renderers, not through a booted Hono app.
Reach: any host whose /data is served by the dispatcher catch-all, and every client of the other REST doors above. A client error answers as a server fault, or names the column key, depending on the door. That is the inconsistency AGENTS.md's "one operation, one implementation" rule exists for.
Proposed direction (the dev's option A, not ruled)
The engine translates a driver NOT NULL refusal on a field the object declares into its own ValidationError, built the way the record validator builds required, so it is labelled and localized by construction. Every door then answers 400 VALIDATION_FAILED with the label: RestServer, the dispatcher twin, batch and import rows, and MCP.
- The engine already holds the field map and the caller's localization context.
- One translation covers every door, with no consumer-side reclassification. That is the contract-first direction.
- PR fix(types,rest): a /data write's NOT NULL refusal names the field by its label #24759's REST-side relabel (
packages/rest/src/not-null-refusal-label.ts) would then cover only non-engine paths, or retire. - Cost: an
objectqlchange, and the dispatcher door's status moving from500to400, which is a wire change at that door.
Alternatives the dev weighed:
- B: the dispatcher exits run
mapDataErroron raw driver errors. This moves that door's status for every driver-text class at once. - C: record the divergence and leave it.
Governing text
- ADR-0113 D1/D2:
requiredandstorage.notNullare kept apart on purpose, so a healthy schema reaches the driver's refusal. - ADR-0112: declared envelopes; a client refusal is not a
500. - AGENTS.md: two implementations of one operation that disagree, so align them.
Dedupe
I ran semantic searches on this repo: "dispatcher data door NOT NULL constraint driver error answers 500 INTERNAL_ERROR instead of 400 validation" and "HttpDispatcher raw driver error not classified mapDataError parity with RestServer". The nearest hits are all closed and none covers this class: #23491 (the dispatcher PATCH request-schema gate), #8016 (the direct-mount door and coded 4xx), #23532 (the hono catch-all leak filter), #24662 (an unevaluable hook condition).
Dedupe words: dispatcher data door NOT NULL 500; INTERNAL_ERROR not-null constraint hono catch-all; driver refusal classification dispatcher parity mapDataError; SQLITE_CONSTRAINT_NOTNULL dispatcher; batch import rows not null column key label.
Generated by Claude Code
Activity