Skip to content

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

Open

Description

@objectstack-fleet

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 }), and POST /data/OBJECT without the field, let the raw SqliteError (SQLITE_CONSTRAINT_NOTNULL) out of dispatch().
  • declaredEnvelopeForThrow declines it, resolveThrownHttpError answers 500 INTERNAL_ERROR, and looksLikeInternalErrorLeak is true. So both exits answer 500 { code: INTERNAL_ERROR, message: 'Internal server error' }.
  • RestServer answers 400 VALIDATION_FAILED for 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 objectql change, and the dispatcher door's status moving from 500 to 400, which is a wire change at that door.

Alternatives the dev weighed:

  • B: the dispatcher exits run mapDataError on 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: required and storage.notNull are 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

  1. added
    bugSomething isn't working
    area:recordsBusiness objects, records, the views that show data, usable forms, search
    and removed on Oct 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:recordsBusiness objects, records, the views that show data, usable forms, searchbugSomething isn't workingdomain:clipm:queuepriority:p2Medium: important, M3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions