Skip to content

Keep a tRPC fault's text off the browser - #141

Merged
heliocosta-dev merged 1 commit into
mainfrom
fix/trpc-error-leak
Oct 16, 2026
Merged

heliocosta-dev merged 1 commit into
mainfrom
fix/trpc-error-leak

Conversation

@heliocosta-dev

Copy link
Copy Markdown
Member

The leak

When a dashboard procedure threw an unexpected error, for example a failed database query, the 500 sent to the browser carried that error's own text. tRPC copies an unknown error's message into the INTERNAL_SERVER_ERROR it builds. So a drizzle Failed query: select … params: … reached any signed-in user, with SQL, table and column names and query parameters. Outside production the response also carried the stack, which repeats the message. Nothing in the app set an errorFormatter, so tRPC's default shape went out as is.

The fix

  • apps/web/src/server/trpc.ts: the router's errorFormatter swaps the message of every INTERNAL_SERVER_ERROR for a fixed one: "Something went wrong. Try again, or contact support if it keeps happening." Only next dev (NODE_ENV=development) keeps the original message and stack. The same flag goes to tRPC's isDev, because its default (any NODE_ENV except production) would send the stack in tests.
  • Refusals keep their words. Every other code (BAD_REQUEST, FORBIDDEN, NOT_FOUND, CONFLICT, TOO_MANY_REQUESTS, PRECONDITION_FAILED, …) passes through unchanged, zod validation messages included.
  • apps/web/src/app/api/trpc/[trpc]/route.ts: onError now also console.errors the fault, as the REST API's onError does. Until now it only reached error tracking, so an instance without SENTRY_DSN recorded it nowhere. Reporting is unchanged: captureError still gets the original error with its procedure and team.
  • settings.invitations.resend: this was the one deliberate 500 with copy of its own, the localized "the invitation email could not be sent" its dialog shows. It now answers SERVICE_UNAVAILABLE, so the dialog keeps that message. Side effect: this refusal is no longer sent to error tracking (onError reports 500s only). The send error itself is still logged where it happens, the same as the invite-create path, which never reported it.

Public REST API and MCP

Both were already generic, so neither changes here:

  • apps/api app.onError answers any unhandled error with 500 internal_server_error "An unexpected error occurred", after logging and reporting it. apps/api/test/error-tracking.test.ts already pins that body.
  • MCP tools reach data only through in-process REST calls (app.fetch), so a fault reaches the agent as that same generic body. If token verification fails with something other than an OAuth error, the SDK answers 500 server_error "Internal Server Error".

Tests

apps/web/test/error-tracking.test.ts drives the real /api/trpc route and router:

  • A procedure whose query throws a raw Failed query: select "secret" … error gets a 500 whose body is the generic message, with no SQL text and no stack. This fails without the fix.
  • The original error reaches both the error tracker and the log.
  • A BAD_REQUEST refusal keeps its own message and is not reported.

pnpm lint, pnpm typecheck and the web suite (1218 tests) pass.

🤖 Generated with Claude Code

A procedure that threw an unexpected error, a failed database query for
one, answered the dashboard with that error's own message: tRPC copies an
unknown error's message into the INTERNAL_SERVER_ERROR it builds, so the
SQL, table and column names and query parameters reached any signed-in
user. Outside production the response carried its stack too.

The router's error formatter now answers every INTERNAL_SERVER_ERROR with
a general message, and only `next dev` keeps the original message and
stack. Refusals with any other code keep their message, validation
details included. The route's onError now also logs the error, which until
now reached only error tracking, and so nowhere without SENTRY_DSN.

The invitation resend's send failure was the one deliberate 500 with copy
of its own, localized for its dialog; it answers SERVICE_UNAVAILABLE now,
so the dialog still says the email could not be sent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@heliocosta-dev
heliocosta-dev merged commit 63cbe8d into main Oct 16, 2026
5 checks passed
@heliocosta-dev
heliocosta-dev deleted the fix/trpc-error-leak branch October 16, 2026 18:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant