fix(host-service): treat a socket torn down mid-request as the cloud being unreachable

Merged
SA
2mo ago
+2-0

Problem

host.info calls the cloud API to resolve the organization. When that request dies in transport, the host-service already knows not to report it: #6735 added rethrowCloudUnreachable, which walks the cause chain for a transport code and rethrows as SERVICE_UNAVAILABLE so the tRPC Sentry middleware leaves it alone.

One transport code was missing from that set. A machine whose socket to the API was torn down mid-request surfaced as a fresh host-service INTERNAL_SERVER_ERROR issue, i.e. reported as our bug.

Root cause

The failure arrives as the usual three-link chain — TRPCClientError("fetch failed") → TypeError("fetch failed") → a Node socket error carrying code: "ENOTCONN". That is the identical shape to the read ECONNRESET case the classifier already covers (and which its tests already name), but ENOTCONN was never in UNREACHABLE_CODES, so findUnreachableCause returned null and the error fell through to the middleware as a 500.

The middleware's contract is explicit: anything still INTERNAL_SERVER_ERROR at that boundary is a bug, and the remedy is to fix the missing translation at the throw site rather than filter at the middleware. This is that missing translation.

Fix

One entry added to the existing code set, plus the matching row in the existing table-driven test. Nothing else: only the code this issue actually produced is added. Sibling codes that have never been observed here (EPIPE, ECONNABORTED, …) are deliberately left out — a classifier that matches more than the condition it names would silence real failures, which is why the set is grown from observed evidence one code at a time.

ENOTCONN on a read means the socket was not connected when the read happened: the peer, a proxy, or the network between the user and the API dropped it. It cannot describe a defect in host-service's own call, and the only errors that reach this classifier are the ones thrown by that single cloud API call.

No behaviour change for any consumer: the desktop's readHostServiceInfo already catches a failing host.info and omits the optional fields, and isHostServiceConnectionError already treats SERVICE_UNAVAILABLE as retryable/connection-level.

Verification

Test row added first, and confirmed failing against unchanged source:

(fail) rethrowCloudUnreachable > ENOTCONN is the cloud being unreachable [0.09ms]
Expected: "SERVICE_UNAVAILABLE"
Received: undefined
 21 pass
 1 fail

After the one-line source change:

$ bun test src/trpc/router/host/
 22 pass
 0 fail
 39 expect() calls
Ran 22 tests across 1 file. [23.00ms]
$ bunx biome check packages/host-service/src/trpc/router/host/cloud-api-error.ts packages/host-service/src/trpc/router/host/cloud-api-error.test.ts
Checked 2 files in 15ms. No fixes applied.

$ cd packages/host-service && bun run typecheck
$ tsc --noEmit --emitDeclarationOnly false
(clean)

No user-facing strings touched, so check:i18n does not apply.

Refs HOST-SERVICE-6S


Summary by cubic

Fixes host.info reporting ENOTCONN socket errors as internal server errors. A socket torn down mid-request to the cloud API is now classified as the cloud being unreachable and rethrown as SERVICE_UNAVAILABLE, so it no longer surfaces as a host-service bug. Adds ENOTCONN to the unreachable codes set plus a matching test row. No consumer behavior changes. Fixes HOST-SERVICE-6S.

Summary by CodeRabbit

  • Bug Fixes
    • Socket connection failures are now correctly reported as a service unavailable error instead of an internal server error.
    • The original error message is preserved for improved troubleshooting.