fix(host-service): classify missing cloud project in setup as NOT_FOUND

Merged
SA
3mo ago
+44-4

Problem

project.setup files a steady trickle of INTERNAL_SERVER_ERROR reports (HOST-SERVICE-16, 14 events lifetime, 2 in the last 24h, release 1.20.2). The captured stack contains only tRPC client transport frames (TRPCClientError.from), which is why the first-party site was hard to place from the event alone; the trpc_path tag project.setup is what identifies it.

Root cause

Setting a project up on a device falls back to a cloud lookup when the project is neither already in the local host database nor accompanied by caller-supplied origin coordinates:

const cloudProject =
  existing || input.origin
    ? null
    : await ctx.api.v2Project.get.query({ organizationId: ctx.organizationId, id: input.projectId });

When the cloud has no such project — the ordinary outcome for a project deleted cloud-side, or for an older client trying to adopt one it cannot see — upstream v2Project.get rejects NOT_FOUND ("Project not found") via requireOrgScopedResource. Nothing at this boundary caught it, so the rejection propagated out of the mutation and the Sentry middleware, which reports every INTERNAL_SERVER_ERROR reaching it, filed it as a bug. This is an expected domain state, not a bug, so the missing classification belongs at the throw site.

Fix

Wrap that one upstream call in fetchCloudProjectForSetup. An upstream not-found is rethrown as a typed TRPCError with code NOT_FOUND and cause { kind: "CLOUD_PROJECT_NOT_FOUND" }, so the Sentry middleware never sees a 500. Everything else — network failures, cloud 5xx, auth errors — propagates untouched and keeps reporting.

Detection is a name check (error.name === "TRPCClientError"), not instanceof: errors arriving from the upstream client cross a bundle boundary and lose their prototype. The not-found condition matches the structured data.code, falling back to a message check only when no structured code is present. The upstream message text is preserved verbatim, so user-facing copy is unchanged. Behaviour is otherwise identical — this is a classification change only.

No Sentry-side suppression, no beforeSend filter, no middleware allowlist.

Verification

bunx biome check packages/host-service/src/trpc/router/project/project.ts:

Checked 1 file in 11ms. No fixes applied.

cd packages/host-service && bun run typecheck:

$ tsc --noEmit --emitDeclarationOnly false

bun test src/trpc/router/project/project-import.test.ts (existing tests over the touched router):

bun test v1.3.14 (0d9b296a)

 3 pass
 0 fail
 16 expect() calls
Ran 3 tests across 1 file. [1159.00ms]

Refs HOST-SERVICE-16

https://claude.ai/code/session_70334aac-77cb-4a5f-86c0-8cd388bf20a0


Summary by cubic

Classifies a missing cloud project during setup as NOT_FOUND instead of INTERNAL_SERVER_ERROR, reducing Sentry noise and returning a typed not-found to clients. Old behavior: unhandled upstream NOT_FOUND bubbled as 500. New behavior: TRPCError with code NOT_FOUND and cause { kind: "CLOUD_PROJECT_NOT_FOUND" }. No user-facing copy change.

  • Wrap the cloud lookup in fetchCloudProjectForSetup in packages/host-service, rethrowing a typed NOT_FOUND.
  • Detect upstream not-found via error.name === "TRPCClientError" and data.code === "NOT_FOUND", with a message fallback; preserve upstream message.
  • Scope: only the setup path when the project is not in the local DB and no origin is provided. Network/auth/5xx still propagate and report.

Summary by CodeRabbit

  • Bug Fixes
    • Improved handling of missing cloud projects during legacy setup lookups.
    • Missing projects are now consistently reported as “Not Found,” while unrelated errors continue to be surfaced unchanged.