fix(client): normalize both server error envelopes so err.code / err.fields mean one thing (#3918 follow-up) - #3927
Merged
os-zhuang merged 2 commits intoJul 29, 2026
Conversation
…fields mean one thing (#3918 follow-up) Two envelopes are in play and they disagree about where the semantic code and the per-field list live: @objectstack/rest, flat: { error, code: 'VALIDATION_FAILED', fields: [...] } runtime dispatcher, wrapped: { success: false, error: { message, code: 400, details: { code: 'VALIDATION_FAILED', fields: [...] } } } `error.code` in the WRAPPED form is the HTTP status, not a semantic code. The client read it straight through, so `err.code` was the number 400 where the flat envelope gave 'VALIDATION_FAILED' — the branch our own docs teach (`if (err.code === 'VALIDATION_FAILED') err.fields.forEach(…)`) never matched on a dispatcher-served surface, and the field list #3918 put on the wire for those routes was unreachable at `err.details.error.details.fields`. Normalized at the throw site: - `err.code` is always the semantic STRING — flat `code`, else wrapped `error.details.code`, else a string `error.code`. A numeric value is never reported as a code; the status stays on `err.httpStatus`. - `err.fields` is the per-field list from either envelope, left UNSET (not []) when the server sent none, so `if (err.fields)` tests "field-anchored". - `err.details` prefers a top-level `details` (unchanged), then the wrapped envelope's own `details`, then the whole body — the flat envelope is unaffected. Nothing in this repo read `err.code` as a number, but a consumer that branched on `err.code === 400` should move to `err.httpStatus === 400`. Documented in content/docs/api/client-sdk.mdx, which enumerates the attached properties and now covers `err.fields`. Covered by 8 cases in client.test.ts; the 4 that target the wrapped envelope fail on the pre-fix source. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LeZ7DmSKjiKmQzYh8L5CJS
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 13 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
…d envelope The docs-drift check on #3927 flagged `api/error-catalog.mdx`, and unlike the rest of the advisory list it was a real hit: its "Client-Side Error Handling" callout enumerates exactly what the SDK attaches to a thrown error, which this branch changes. Two things were already wrong there before this branch, and one is new: - The callout said the client attaches "only code, category, httpStatus, retryable, and details — not fieldErrors", and then the TypeScript example immediately called `apiError.fieldErrors?.forEach(...)`. The example could never have worked against the very SDK the callout describes. - Nothing said `code` is a string while the status lives on `httpStatus` — the distinction this branch makes true for both wire formats. Now: the callout lists `fields` among the attached properties, states the `fields` (wire) vs `fieldErrors` (spec contract) naming split, notes that `fields` is left unset rather than empty, and spells out the code-vs-status rule. The example reads `apiError.fields` and widens its cast to `EnhancedApiError & { fields?: FieldError[] }` so it actually typechecks. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LeZ7DmSKjiKmQzYh8L5CJS
This was referenced Jul 29, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Third and last piece of the #3918 chain (after #3920 and #3925). This is the one that makes the original report's symptom — no per-field error display on dispatcher-served forms — actually go away for a caller.
The bug
#3920 put
fields[]on the wire for dispatcher-served routes. A caller still couldn't get at it, because two envelopes are in play and they disagree about where the semantic code and the field list live:error.codein the wrapped form is the HTTP status, not a semantic code.packages/client/src/index.tsread it straight through (errorBody?.code || errorBody?.error?.code), soerr.codewas the number400where the flat envelope gave'VALIDATION_FAILED'.The consequence is that the branch our own documentation teaches —
content/docs/protocol/kernel/http-protocol.mdx:923—…never matched on a dispatcher-served surface, and the field list was sitting unreachable at
err.details.error.details.fields.The fix
Normalized once, at the throw site:
err.codeis always the semantic string. Read from the flatcode, else the wrappederror.details.code, else a stringerror.code. A numeric value is never reported as a code. The HTTP status is onerr.httpStatus, where it always was.err.fieldsis the per-field list from either envelope. Left unset (not[]) when the server sent none, soif (err.fields)is a safe test for "this failure is field-anchored".err.detailsprefers a top-leveldetails(unchanged), then the wrapped envelope's owndetails, then the whole body. The flat envelope has no top-leveldetails, so it keeps falling through to the whole body exactly as before — only the wrapped shape changes, and only from "the entire response" to the structured object it actually carries.content/docs/api/client-sdk.mdxenumerates the properties attached to a thrown error, so it's updated in the same commit:err.fieldsdocumented with a worked example, plus a note thaterr.codeis always semantic and the status is only ever onerr.httpStatus.Verification
client.test.tscovering both envelopes symmetrically: semantic code from each,fields[]from each,fieldsunset when absent, no numeric code even with nodetailsto fall back on, and threedetailscases (flat unchanged / wrapped structured / top-leveldetailsstill honoured).The behaviour change — please sanity-check this call
Code reading
err.codefrom a dispatcher-served route previously got a number and now gets a string (orundefinedwhere the server sent no semantic code).My reasoning for treating this as a fix rather than a break:
err.httpStatuswas always the correct and documented source for the status,err.codecarrying two different types depending on which surface answered was the actual defect, and I grepped the repo — nothing branches on a numericerr.code. But a downstream consumer outside this repo that diderr.code === 400would neederr.httpStatus === 400, and that's called out in the changeset.If you'd rather keep the numeric value reachable, say so and I'll leave
err.codealone and expose the semantic one as a new property instead — but then the documentederr.code === 'VALIDATION_FAILED'branch stays broken for dispatcher routes, which is what this chain set out to fix.Generated by Claude Code