Skip to content

fix(runtime): serve VALIDATION_FAILED as 400 with fields[] from both dispatcher exits (#3918) - #3920

Merged
os-zhuang merged 1 commit into
mainfrom
claude/dispatcher-validation-error-fields-xo6zx9
Jul 29, 2026
Merged

fix(runtime): serve VALIDATION_FAILED as 400 with fields[] from both dispatcher exits (#3918)#3920
os-zhuang merged 1 commit into
mainfrom
claude/dispatcher-validation-error-fields-xo6zx9

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Closes #3918.

The bug

ValidationError (objectql's record/rule validators) carries .code = 'VALIDATION_FAILED' and .fields[] — one entry per offending field. It deliberately carries no .status, no .statusCode, and no .issues: it's a plain domain error, and deciding it means "400" is the HTTP boundary's job.

@objectstack/rest has always done that (mapDataError → 400 with fields[]). The runtime dispatcher's two error exits did not, because each read exactly the properties this error lacks:

exit what went wrong
http-dispatcher.tserrorFromThrown (the returned-error path: /meta save, /packages publish, …) no .status → fell back to the caller's fallbackStatus (500 on /packages); details built from .issues alone → fields[] dropped
dispatcher-plugin.tserrorResponseBase (the thrown-error path: every route the plugin mounts — /analytics, /packages, /i18n, /storage, /automation, /auth, /notifications, /mcp, …) same 500 fallback, and a body of only {message, code}. Landing on 5xx then dragged the message through the #3867 leak sanitiser — so a user typing a bad email address got back a 500 "Internal server error": no status a client could act on, no message worth showing, nothing to attach to the input

Net effect: per-field error display was foreclosed on every surface the runtime dispatcher serves.

The fix

New packages/runtime/src/validation-failure.ts holds one predicate shared by both exits, duck-typed on code === 'VALIDATION_FAILED' || name === 'ValidationError' — the same both-ways test mapDataError uses. So the runtime takes no dependency on objectql, and a hook that throws { code: 'VALIDATION_FAILED', fields } by hand is served identically.

Both exits now answer the way rest-server does: status 400, with the error's fields[] passed through verbatim in details alongside code: 'VALIDATION_FAILED'.

Two deliberate calls worth a reviewer's attention:

  • An explicit .status / .statusCode still wins. 400 is supplied only as the fallback that used to be 500. rest-server hardcodes 400; keeping the dispatcher's existing precedence made this a strictly smaller change, and it's equivalent for a real ValidationError (which has neither).
  • fields[] goes in error.details, not at the body's top level. The dispatcher's envelope is {success, error: {message, code, details}} throughout, where code is the HTTP status and structured detail lives in details; rest-server's flat {error, code, fields} shape doesn't exist here. This matches rest-server's semantics (400 + fields[] present) inside the dispatcher's own envelope rather than reproducing its literal layout. Happy to mirror the list at error.fields too if you'd rather — it's a one-liner.

Non-validation errors are untouched: same status, same message sanitising, same details, and errorResponseBase still emits its exact two-key body for them.

Verification

  • 15 new tests in dispatcher-validation-error.test.ts, driving both exits through real routes rather than calling the private helpers — /packages/:id/publish-drafts for the returned path (the 500-fallback caller, i.e. where the downgrade actually bit), POST /analytics/query for the thrown one.
  • 11 of those 15 fail against the pre-fix source — verified by reverting the two source files and re-running, so the suite isn't vacuous.
  • Full runtime suite green: 735 tests / 52 files. Package builds; eslint clean on the changed files.
  • tsc --noEmit on the package reports 11 pre-existing TS6133 unused-variable errors in action-execution.ts, domains/mcp.ts and domains/meta.ts — files this change doesn't touch. None in the changed files.

Changeset

Added a patch changeset. AGENTS.md says pure bug fixes don't require one, but the wire behaviour visibly changes for consumers (500 → 400, new details), so it seemed worth landing in the CHANGELOG. Easy to drop if you disagree.


Generated by Claude Code

…dispatcher exits (#3918)

`ValidationError` carries `.code = 'VALIDATION_FAILED'` and `.fields[]`, and
deliberately carries no `.status`, no `.statusCode` and no `.issues` — deciding
it means "400" is the HTTP boundary's job. `@objectstack/rest` has always done
that (`mapDataError`). The runtime dispatcher's two error exits did not, because
each read exactly the properties this error lacks:

- `HttpDispatcher.errorFromThrown` fell back to the caller's `fallbackStatus`
  for want of a `.status`, and built `details` from `.issues` alone — so
  `fields[]` was dropped.
- `dispatcher-plugin`'s `errorResponseBase` took the same 500 fallback, and its
  body was only `{message, code}`. Landing on 5xx then dragged the message
  through the #3867 leak sanitiser, so a bad email address came back as a 500
  "Internal server error" with nothing to attach to the offending input.

Both exits now answer the way rest-server does: status 400, with `fields[]`
passed through verbatim in `details` alongside `code: 'VALIDATION_FAILED'`, so
every surface the dispatcher serves can do per-field error display.

The predicate lives in the new `validation-failure.ts` — one definition shared
by both exits, duck-typed on `code`/`name` exactly like `mapDataError`, so the
runtime takes no dependency on objectql and hand-thrown errors of the same shape
are served identically.

An explicit `.status`/`.statusCode` still wins; 400 is supplied only as the
fallback that used to be 500. Non-validation errors keep their status, message
sanitising and `details` untouched, and `errorResponseBase` still emits its
exact two-key body for them.

Covered by `dispatcher-validation-error.test.ts`, which drives both exits
through real routes (`/packages/:id/publish-drafts` for the returned path,
`POST /analytics/query` for the thrown one); 11 of its 15 cases fail on the
pre-fix source.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LeZ7DmSKjiKmQzYh8L5CJS
@vercel

vercel Bot commented Jul 29, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Jul 29, 2026 9:24am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/m labels Jul 29, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/runtime.

18 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/api/client-sdk.mdx (via packages/runtime)
  • content/docs/api/index.mdx (via @objectstack/runtime)
  • content/docs/api/wire-format.mdx (via @objectstack/runtime)
  • content/docs/automation/hook-bodies.mdx (via @objectstack/runtime)
  • content/docs/concepts/north-star.mdx (via packages/runtime)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/runtime)
  • content/docs/deployment/index.mdx (via @objectstack/runtime)
  • content/docs/deployment/production-readiness.mdx (via @objectstack/runtime)
  • content/docs/deployment/single-project-mode.mdx (via @objectstack/runtime)
  • content/docs/deployment/vercel.mdx (via @objectstack/runtime)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/runtime)
  • content/docs/permissions/authentication.mdx (via @objectstack/runtime)
  • content/docs/permissions/authorization.mdx (via packages/runtime)
  • content/docs/plugins/packages.mdx (via @objectstack/runtime)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/runtime)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/runtime)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/runtime)
  • content/docs/releases/implementation-status.mdx (via @objectstack/runtime)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[17.0.0-rc.0] Runtime dispatcher error paths drop ValidationError.fields[] and downgrade status to 500 (rest-server maps it correctly)

2 participants