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:

exitwhat 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

vercelBot commented Jul 29, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredJul 29, 2026 9:24am

Request Review

@github-actionsgithub-actionsBot 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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

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

@os-zhuang@claude