fix(web): classify rejected inputValidator requests as 400, not 500 - #4587
fix(web): classify rejected inputValidator requests as 400, not 500#4587geclos wants to merge 3 commits into
Conversation
TanStack Start's execValidator throws a plain Error whose message is the JSON-encoded Standard Schema issues array when a .inputValidator() (zod) rejects input, instead of an HttpError/ZodError. errorStatus() only recognized HttpError-shaped exceptions, so every rejected .inputValidator() call across the app fell through to the 500 default and was recorded as a false server fault in Datadog Error Tracking, defeating the isExpectedClientError filter built specifically to keep this noise out. Duck-type the Standard Schema issues shape in errorStatus() and classify it as 400, and route recordServerFnError's status through the shared errorStatus() helper instead of a narrower inline check. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KGQjNxrRnKpLJ22HRJTgpB
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
|
Scheduled Datadog triage (2026-09-14): this fix is still correct and the underlying Datadog issue ( Once merged, #4488 can be closed as superseded (same fix, opened first, never merged). Generated by Claude Code |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
Review summaryReviewed the diff ( What I checked:
Findings: one inline note (non-blocking) — No correctness, security, or architecture-boundary issues found otherwise. The change is narrowly scoped, well-tested, and matches the file's existing "classify status, gate on it" pattern. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 57a78cd7d1
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
The Standard Schema spec allows an issue to omit path entirely for a root-level error, not just supply an empty array. isStandardSchemaIssue required Array.isArray(path) unconditionally, so such an issue fell through the duck-type check to the 500 default — the exact misclassification this PR fixes, for that case. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KGQjNxrRnKpLJ22HRJTgpB
|
📄 Generated an HTML diff-review artifact explaining this PR's change (background, intuition, code walkthrough, and a 5-question quiz).
🤖 Generated with Claude Code |
…uest ZodError.message is JSON.stringify(issues, ..., 2) too, since it subclasses Error, so the duck-typed check matched any schema.parse() failure anywhere in a handler (e.g. a repository mapper validating a persisted row) as an expected 400 client error and hid it from Datadog. TanStack's execValidator throws a literal `new Error(...)`, not a subclass, so require an exact Error constructor match to exclude ZodError and other Error subclasses. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KGQjNxrRnKpLJ22HRJTgpB
What does this PR do?
Fixes a false-positive noise source in Datadog Error Tracking: server functions that reject via
.inputValidator()(zod) are recorded as500server faults instead of expected400client errors.Datadog issue addressed:
786d9ad4-8069-11f1-ab43-da7ad0900005—webservice, error typeError, 240 occurrences over the last 7 days (recurring since first seen ~54 days ago), most recently 2026-09-07T04:18 UTC. Sampled fromcompleteOnboarding'sorganizationNamevalidation failing with"Please enter an organization name".Root cause:
apps/web/src/middlewares/server-fn-error.tshas a deliberateisExpectedClientErrorfilter so 4xx-class errors (bad input, no access, not logged in) are excluded from Error Tracking, keeping the signal focused on real 5xx bugs. It classifies viaerrorStatus(), which only recognizedHttpError-shaped exceptions (objects withhttpStatus/httpMessage).TanStack Start's
execValidator(@tanstack/start-client-core) does not throw anHttpErrororZodErrorwhen a Standard SchemainputValidatorrejects input — it throws a plainErrorwhose message isJSON.stringify(issues):errorStatus()didn't recognize this shape, so it fell through to the500default, and every rejected.inputValidator()call across the app (315+ call sites) got recorded into Datadog as a false server fault — exactly the noise the filter was built to prevent. The client already handles this gracefully viaextractFieldErrors/createFormSubmitHandler(inline field errors, no raw error surfaced to the user), so this is purely an observability bug, not a user-facing one.This isn't specific to
completeOnboarding—errorStatus()is the single shared classification point for every server function using.inputValidator(), so the fix resolves the false-positive app-wide, not just for the observed call site.Note on prior art: an earlier draft PR (#4488) proposed the identical fix back on 2026-08-21 but was never merged, so the issue kept accumulating occurrences for another ~17 days (74 → 240). This PR carries the same fix, independently re-verified against the current
developmentHEAD, with tests re-run rather than assumed. Recommend closing #4488 as superseded once this merges.Fix: duck-type the Standard Schema issues array shape (
{ message: string, path: array }[]) inerrorStatus()and classify it as400, the same wayNotFoundError/UnauthorizedErrorare already excluded. Also routedrecordServerFnError's status through the sharederrorStatus()helper instead of a narrower inlinehttpError ? e.httpStatus : 500check, so both call sites (recordServerFnErrorandrecordRequestError) stay in sync.No behavior change for users: the client-bound error message is untouched, so
extractFieldErrorscontinues to parse field errors identically. This only changes what gets reported to Datadog (skipped) and the log level (warninstead oferror).Related issue (if applicable)
N/A — found via scheduled Datadog Error Tracking triage, not a filed issue.
How was this tested?
server-fn-error.test.ts:Erroris classified400/isClientError: trueand not recorded to the span, while the client-bound message still round-trips correctly forextractFieldErrors.500(no over-broad matching).pnpm --filter @app/web test— full suite green except 2 pre-existing, unrelated failures inphone-countries.test.ts(ICU timezone data differences in this sandbox; untouched by this change).pnpm --filter @app/web typecheck(tsgo) — clean.pnpm exec biome checkon changed files — clean.Verification after deploy: occurrences of issue
786d9ad4-8069-11f1-ab43-da7ad0900005(and the equivalent pattern on any other.inputValidator()-guarded server function) should stop appearing in Error Tracking going forward, since new occurrences will be classified as400at ingestion.Checklist
🤖 Generated with Claude Code
https://claude.ai/code/session_01KGQjNxrRnKpLJ22HRJTgpB
Generated by Claude Code
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.