Skip to content

fix(core): fail rpc.internal as a typed error on every path - #46902

Closed
kitlangton wants to merge 2 commits into
v2from
rpc-error-contract
Closed

fix(core): fail rpc.internal as a typed error on every path#46902
kitlangton wants to merge 2 commits into
v2from
rpc-error-contract

Conversation

@kitlangton

Copy link
Copy Markdown
Contributor

Why

Rpc.client(definition) is typed RpcClient<D, Rpc.SystemError, ...>, and SystemError includes "rpc.internal", but core never produced it. A handler that died, threw, failed with a raw value (the Promise adapter path), or used an undeclared context.error(...) type surfaced as an uncatchable defect for in-process callers (plugin → plugin), while an HTTP caller of the same handler got a catchable { type: "rpc.internal" } from the server's catchDefect. The two transports disagreed about the error contract, and the server's safety net also echoed arbitrary handler exception text to HTTP clients, which no other handler does.

Separately, #46639 deleted every test for the RPC subsystem added in #46105 (core/test/rpc.test.ts, core/test/plugin/rpc-{effect,promise}.test.ts, server/test/rpc.test.ts). This PR restores them first so the fix lands against a real regression suite.

What Changes

packages/core/src/rpc.tsRpc.call now converts every non-declared handler outcome into a typed Rpc.Failure with type: "rpc.internal":

// before: uncatchable
dies       Effect.die(error)
throws     defect passes through untouched
raw fail   Effect.die(error)
undeclared→ Effect.die(new Error("Undeclared RPC error: x"))

// after: one typed failure, logged once at the conversion point
dies / throws / raw fail  { type: "rpc.internal", message: "RPC call failed" }
undeclared                { type: "rpc.internal", message: "Undeclared RPC error: x" }

Each conversion runs Effect.logError("rpc handler failed", { rpc, method, error }) exactly once so the underlying cause is not lost. The Effect.catchEffect.catchDefect order means a declared error whose data fails its own schema also lands as rpc.internal (it is a handler bug, not a caller error). rpc.invalid_output is unchanged. encodeError was inlined into the single call site.

packages/server/src/handlers/rpc.ts — maps core's rpc.internal (alongside rpc.invalid_output) to RpcInternalError so the HTTP status stays 500. The catchDefect safety net remains but now returns the fixed message "RPC call failed" and logs the defect server-side instead of echoing error.message. Rpc.Failure → 400 mapping and all status codes are unchanged.

Not touched: Rpc.define schema surface, method/event naming, packages/client, packages/protocol (no HttpApi change, no bun run generate), the Promise adapter.

Restored tests

All restored from 6a2c3e91c7 and adapted to HEAD (Plugin.awaitActivation instead of PluginSupervisor.flush, revision instead of version on Plugin.Generation, Node.replace(layer) / LayerNode.compile(root, { replacements }) for the layer-node API, ctx.location instead of (yield* ctx.agent.list()).location, tmpdirScoped, no aliased imports).

File Cases restored Cases dropped Cases skipped
packages/core/test/rpc.test.ts 14 (+1 new) 0 0
packages/core/test/plugin/rpc-effect.test.ts 2 0 0
packages/core/test/plugin/rpc-promise.test.ts 4 0 0
packages/server/test/rpc.test.ts 3 0 0

Every restored assertion held on HEAD before the source change, so nothing else in the RPC subsystem regressed since #46105. One assertion was updated by the fix itself: rpc-promise.test.ts previously asserted client.defect() rejects with the raw "handler defect" Error and now asserts the typed { type: "rpc.internal", message: "RPC call failed" } rejection.

New coverage for the contract:

  • core/test/rpc.test.ts — dies / throws / raw fail / undeclared error → in-process client(def).method() fails (not dies) with rpc.internal, is recoverable via Effect.catchIf, and each conversion logs once. Confirmed it dies on HEAD before the fix.
  • server/test/rpc.test.ts — the same defect over HTTP returns 500 with "RPC call failed" and no exception text; undeclared error returns 500 with Undeclared RPC error: <type>.

Verification

  • bun typecheck clean in packages/core, packages/server, packages/plugin
  • oxlint clean on all changed files
  • Restored + new tests --rerun-each 3: core 63/63 pass (3 files), server 9/9 pass
  • Full bun run test in packages/core: 4018 pass, 39 skip, 1 fail — snapshot.test.ts > treats fatal ignore checks as unavailable captures, which passes in isolation on both this branch and clean origin/v2 (order-dependent, unrelated to RPC)
  • Full bun run ../core/script/test.ts in packages/server: 53 pass, 3 skip (pre-existing persistent-pty binary smoke skips), 0 fail

@kitlangton

Copy link
Copy Markdown
Contributor Author

Superseded by #46946, which keeps the handler-error fix and adds only seven focused regression cases. All seven fail on the unfixed base; the broader test-suite restoration is left out.

@kitlangton kitlangton closed this Sep 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant